mirror of
https://github.com/Steffo99/festa.git
synced 2024-12-22 14:44:21 +00:00
Rearrange things
This commit is contained in:
parent
6673b9fd15
commit
e8086a7b5f
29 changed files with 577 additions and 588 deletions
17
.vscode/launch.json
vendored
17
.vscode/launch.json
vendored
|
@ -9,13 +9,26 @@
|
|||
"request": "launch",
|
||||
"runtimeArgs": [
|
||||
"run",
|
||||
"app:dev",
|
||||
"dev",
|
||||
],
|
||||
"runtimeExecutable": "yarn",
|
||||
"skipFiles": [
|
||||
"<node_internals>/**"
|
||||
],
|
||||
"type": "node"
|
||||
}
|
||||
},
|
||||
{
|
||||
"name": "Prisma Studio",
|
||||
"request": "launch",
|
||||
"runtimeArgs": [
|
||||
"run",
|
||||
"studio",
|
||||
],
|
||||
"runtimeExecutable": "yarn",
|
||||
"skipFiles": [
|
||||
"<node_internals>/**"
|
||||
],
|
||||
"type": "node"
|
||||
},
|
||||
]
|
||||
}
|
13
components/Avatar.tsx
Normal file
13
components/Avatar.tsx
Normal file
|
@ -0,0 +1,13 @@
|
|||
import Image, { ImageProps } from "next/image";
|
||||
import { HTMLProps } from "react";
|
||||
import classNames from "classnames"
|
||||
|
||||
export function Avatar(props: ImageProps) {
|
||||
return (
|
||||
<Image
|
||||
alt=""
|
||||
{...props}
|
||||
className={classNames(props.className, "avatar")}
|
||||
/>
|
||||
)
|
||||
}
|
|
@ -1,28 +0,0 @@
|
|||
import * as React from "react"
|
||||
|
||||
export interface InputSlug extends React.HTMLProps<HTMLInputElement> {
|
||||
onSlugChange?: (val: string) => void,
|
||||
}
|
||||
|
||||
export function InputSlug(props: InputSlug) {
|
||||
const [text, setText] = React.useState("")
|
||||
|
||||
const handleChange = React.useCallback((event: React.ChangeEvent<HTMLInputElement>) => {
|
||||
props.onChange?.(event)
|
||||
|
||||
let slug = event.target.value.toLowerCase().replaceAll(/[^a-z0-9]/g, "-")
|
||||
props.onSlugChange?.(slug)
|
||||
setText(slug)
|
||||
},
|
||||
[]
|
||||
)
|
||||
|
||||
return (
|
||||
<input
|
||||
type="text"
|
||||
value={text}
|
||||
onChange={handleChange}
|
||||
{...props}
|
||||
/>
|
||||
)
|
||||
}
|
|
@ -6,7 +6,7 @@ export function Postcard() {
|
|||
const [postcard, _] = useDefinedContext(PostcardContext)
|
||||
|
||||
return (
|
||||
<img
|
||||
<Image
|
||||
className="postcard"
|
||||
src={typeof postcard === "string" ? postcard : postcard.src}
|
||||
alt=""
|
||||
|
|
17
components/SectionLoginTelegram.tsx
Normal file
17
components/SectionLoginTelegram.tsx
Normal file
|
@ -0,0 +1,17 @@
|
|||
import { useTranslation } from "next-i18next"
|
||||
import { useCallback, useState } from "react"
|
||||
import { LoginContext } from "../contexts/login"
|
||||
import { useDefinedContext } from "../utils/definedContext"
|
||||
import { TelegramLoginButton } from "./TelegramLoginButton"
|
||||
import { default as axios } from "axios"
|
||||
import { AuthenticatedUserData, TelegramLoginData } from "../types/user"
|
||||
|
||||
|
||||
export function SectionLoginTelegram() {
|
||||
const { t } = useTranslation("common")
|
||||
const [login, setLogin] = useDefinedContext(LoginContext)
|
||||
const [error, setError] = useState<any>(null)
|
||||
|
||||
return <section>
|
||||
</section>
|
||||
}
|
|
@ -1,21 +0,0 @@
|
|||
import { LoginContext } from "../contexts/login";
|
||||
import { useDefinedContext } from "../utils/definedContext";
|
||||
import { UserData } from "../utils/telegram";
|
||||
|
||||
|
||||
export interface TelegramAvatarProps {
|
||||
u: UserData
|
||||
}
|
||||
|
||||
|
||||
export function TelegramAvatar({u}: TelegramAvatarProps) {
|
||||
const [login, _] = useDefinedContext(LoginContext)
|
||||
|
||||
return login ?
|
||||
<img
|
||||
src={u.photo_url}
|
||||
className="avatar-telegram-inline"
|
||||
/>
|
||||
:
|
||||
null
|
||||
}
|
|
@ -1,11 +1,11 @@
|
|||
import { UserData } from "../utils/telegram";
|
||||
import { TelegramLoginData } from "../types/user";
|
||||
import { TelegramAvatar } from "./TelegramAvatar";
|
||||
|
||||
interface TelegramUserLinkProps {
|
||||
u: UserData
|
||||
interface Props {
|
||||
u: TelegramLoginData
|
||||
}
|
||||
|
||||
export function TelegramUser({u}: TelegramUserLinkProps) {
|
||||
export function TelegramUserInline({u}: Props) {
|
||||
|
||||
if(u.username) return (
|
||||
<a href={`https://t.me/${u.username}`}>
|
|
@ -1,41 +0,0 @@
|
|||
import { useTranslation } from "next-i18next"
|
||||
import { useCallback } from "react"
|
||||
import { LoginContext } from "../contexts/login"
|
||||
import { useDefinedContext } from "../utils/definedContext"
|
||||
import { TelegramLoginButton } from "./TelegramLoginButton"
|
||||
import * as Telegram from "../utils/telegram"
|
||||
import axios from "axios"
|
||||
|
||||
|
||||
export function TutorialTelegramLogin() {
|
||||
const { t } = useTranslation("common")
|
||||
const [login, setLogin] = useDefinedContext(LoginContext)
|
||||
|
||||
const onLogin = useCallback(
|
||||
async (data: Telegram.LoginData) => {
|
||||
console.debug("[Telegram] Logged in successfully, now forwarding to the server...")
|
||||
const response = await axios.post("/api/login/telegram", data)
|
||||
console.info(response)
|
||||
},
|
||||
[]
|
||||
)
|
||||
|
||||
if (!login) {
|
||||
return <>
|
||||
<div>
|
||||
{t("introTelegramLogin")}
|
||||
</div>
|
||||
<TelegramLoginButton
|
||||
dataOnauth={onLogin}
|
||||
botName={process.env.NEXT_PUBLIC_TELEGRAM_USERNAME}
|
||||
/>
|
||||
</>
|
||||
}
|
||||
else {
|
||||
return <>
|
||||
<div>
|
||||
|
||||
</div>
|
||||
</>
|
||||
}
|
||||
}
|
|
@ -1,6 +1,5 @@
|
|||
import { useStorageState } from "react-storage-hooks";
|
||||
import { FestaLoginData } from "../types/user";
|
||||
import { createStateContext } from "../utils/stateContext";
|
||||
import * as Telegram from "../utils/telegram"
|
||||
|
||||
|
||||
/**
|
||||
|
@ -10,4 +9,4 @@ import * as Telegram from "../utils/telegram"
|
|||
*
|
||||
* Please note that the data containing in this context is not validated, and will need to be validated by the server on every request.
|
||||
*/
|
||||
export const LoginContext = createStateContext<Telegram.LoginData | null>()
|
||||
export const LoginContext = createStateContext<FestaLoginData | null>()
|
||||
|
|
|
@ -1,6 +1,6 @@
|
|||
import { createStateContext } from "../utils/stateContext";
|
||||
import { StaticImageData } from "next/image";
|
||||
import * as Telegram from "../utils/telegram"
|
||||
import * as Telegram from "../utils/TelegramUserDataClass"
|
||||
|
||||
|
||||
/**
|
||||
|
|
40
hooks/useStoredLogin.ts
Normal file
40
hooks/useStoredLogin.ts
Normal file
|
@ -0,0 +1,40 @@
|
|||
import { useEffect } from "react"
|
||||
import { FestaLoginData } from "../types/user"
|
||||
|
||||
export function useStoredLogin(setLogin: React.Dispatch<React.SetStateAction<FestaLoginData | null>>): void {
|
||||
const thatStorageOverThere = typeof localStorage !== "undefined" ? localStorage : undefined
|
||||
|
||||
useEffect(
|
||||
() => {
|
||||
if(thatStorageOverThere === undefined) return
|
||||
|
||||
const raw = localStorage.getItem("login")
|
||||
if(raw === null) {
|
||||
console.debug("No stored login data found.")
|
||||
return
|
||||
}
|
||||
|
||||
try {
|
||||
var parsed = JSON.parse(raw)
|
||||
}
|
||||
catch(e) {
|
||||
console.error("Failed to parse stored login data as JSON.")
|
||||
return
|
||||
}
|
||||
|
||||
const data = {
|
||||
...parsed,
|
||||
expiresAt: new Date(parsed.expiresAt)
|
||||
}
|
||||
|
||||
if(new Date().getTime() >= data.expiresAt.getTime()) {
|
||||
console.debug("Stored login data has expired, clearing...")
|
||||
thatStorageOverThere.removeItem("login")
|
||||
return
|
||||
}
|
||||
|
||||
setLogin(data)
|
||||
},
|
||||
[thatStorageOverThere]
|
||||
)
|
||||
}
|
26
hooks/useTelegramToFestaCallback.ts
Normal file
26
hooks/useTelegramToFestaCallback.ts
Normal file
|
@ -0,0 +1,26 @@
|
|||
import { default as axios, AxiosError } from "axios"
|
||||
import { useCallback, Dispatch, SetStateAction } from "react"
|
||||
import { ApiError, ApiResult } from "../types/api"
|
||||
import { FestaLoginData, TelegramLoginData } from "../types/user"
|
||||
|
||||
|
||||
export function useTelegramToFestaCallback(setLogin: Dispatch<SetStateAction<FestaLoginData | null>>, setError: Dispatch<SetStateAction<ApiError | null | undefined>>): (data: TelegramLoginData) => Promise<void> {
|
||||
return useCallback(
|
||||
async (data: TelegramLoginData) => {
|
||||
setError(null)
|
||||
|
||||
try {
|
||||
var response = await axios.post<ApiResult<FestaLoginData>>("/api/login?provider=telegram", data)
|
||||
}
|
||||
catch(e) {
|
||||
const axe = e as AxiosError
|
||||
setError(axe?.response?.data as ApiError | undefined)
|
||||
return
|
||||
}
|
||||
|
||||
setLogin(response.data as FestaLoginData)
|
||||
localStorage.setItem("login", JSON.stringify(response.data))
|
||||
},
|
||||
[]
|
||||
)
|
||||
}
|
13
package.json
13
package.json
|
@ -1,14 +1,13 @@
|
|||
{
|
||||
"name": "festa",
|
||||
"name": "@steffo/festa",
|
||||
"version": "0.1.0",
|
||||
"private": true,
|
||||
"scripts": {
|
||||
"app:dev": "next dev",
|
||||
"app:build": "next build",
|
||||
"app:start": "next start",
|
||||
"app:lint": "next lint",
|
||||
"db:dev": "dotenv -e .env.local prisma db push --force-reset",
|
||||
"db:generate": "dotenv -e .env.local prisma generate"
|
||||
"dev": "dotenv -e .env.local prisma db push && dotenv -e .env.local prisma generate && dotenv -e .env.local next dev",
|
||||
"build": "next build",
|
||||
"start": "next start",
|
||||
"lint": "next lint",
|
||||
"studio": "dotenv -e .env.local prisma studio"
|
||||
},
|
||||
"dependencies": {
|
||||
"@prisma/client": "3.14.0",
|
||||
|
|
|
@ -2,42 +2,19 @@ import '../styles/globals.css'
|
|||
import type { AppProps } from 'next/app'
|
||||
import { LoginContext } from '../contexts/login'
|
||||
import { useEffect, useState } from 'react'
|
||||
import * as Telegram from "../utils/telegram"
|
||||
import defaultPostcard from "../public/postcards/adi-goldstein-Hli3R6LKibo-unsplash.jpg"
|
||||
import { Postcard } from '../components/Postcard'
|
||||
import { PostcardContext } from '../contexts/postcard'
|
||||
import { StaticImageData } from 'next/image'
|
||||
import { appWithTranslation } from 'next-i18next'
|
||||
import { useStorageState } from 'react-storage-hooks'
|
||||
import { FestaLoginData } from '../types/user'
|
||||
import {useStoredLogin} from "../hooks/useStoredLogin"
|
||||
|
||||
const dummyStorage = {
|
||||
getItem: () => null,
|
||||
setItem: () => {},
|
||||
removeItem: () => {},
|
||||
};
|
||||
|
||||
const App = ({ Component, pageProps }: AppProps): JSX.Element => {
|
||||
const [login, setLogin] = useState<Telegram.LoginData | null>(null)
|
||||
const [login, setLogin] = useState<FestaLoginData | null>(null)
|
||||
const [postcard, setPostcard] = useState<string | StaticImageData>(defaultPostcard)
|
||||
|
||||
// Ha ha ha. Fooled you again, silly SSR!
|
||||
const thatStorageOverThere = typeof sessionStorage !== "undefined" ? sessionStorage : undefined
|
||||
|
||||
useEffect(
|
||||
() => {
|
||||
if(thatStorageOverThere === undefined) return
|
||||
|
||||
const raw = sessionStorage.getItem("login")
|
||||
if(raw === null) return
|
||||
|
||||
const parsed = JSON.parse(raw) as Telegram.LoginData
|
||||
const response = new Telegram.LoginResponse(parsed)
|
||||
if(!response.isRecent) return
|
||||
|
||||
setLogin(parsed)
|
||||
},
|
||||
[thatStorageOverThere]
|
||||
)
|
||||
useStoredLogin(setLogin)
|
||||
|
||||
return (
|
||||
<PostcardContext.Provider value={[postcard, setPostcard]}>
|
||||
|
|
89
pages/api/login/index.ts
Normal file
89
pages/api/login/index.ts
Normal file
|
@ -0,0 +1,89 @@
|
|||
import { NextApiRequest, NextApiResponse } from "next"
|
||||
import { prisma } from "../../../utils/prismaClient"
|
||||
import { TelegramUserDataClass } from "../../../utils/TelegramUserDataClass"
|
||||
import { default as cryptoRandomString } from "crypto-random-string"
|
||||
import { ApiResult } from "../../../types/api"
|
||||
import { Token, User } from "@prisma/client"
|
||||
import { FestaLoginData } from "../../../types/user"
|
||||
|
||||
|
||||
export default async function handler(req: NextApiRequest, res: NextApiResponse<ApiResult<FestaLoginData>>) {
|
||||
switch (req.method) {
|
||||
case "POST":
|
||||
switch(req.query.provider) {
|
||||
case "telegram":
|
||||
return await loginTelegram(req, res)
|
||||
default:
|
||||
return res.status(400).json({ error: "Unknown login provider" })
|
||||
}
|
||||
default:
|
||||
return res.status(405).json({ error: "Invalid method" })
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
async function loginTelegram(req: NextApiRequest, res: NextApiResponse<ApiResult<FestaLoginData>>) {
|
||||
const botToken = process.env.TELEGRAM_TOKEN
|
||||
if (!botToken) {
|
||||
return res.status(503).json({ error: "`TELEGRAM_TOKEN` was not set up" })
|
||||
}
|
||||
|
||||
const hashExpirationMs = parseInt(process.env.TELEGRAM_HASH_EXPIRATION_MS!)
|
||||
if (!hashExpirationMs) {
|
||||
return res.status(503).json({ error: "`TELEGRAM_HASH_EXPIRATION_MS` was not set up" })
|
||||
}
|
||||
|
||||
const tokenExpirationMs = parseInt(process.env.FESTA_TOKEN_EXPIRATION_MS!) // Wrong typing?
|
||||
if (!tokenExpirationMs) {
|
||||
return res.status(503).json({ error: "`FESTA_TOKEN_EXPIRATION_MS` was not set up" })
|
||||
}
|
||||
|
||||
try {
|
||||
var userData: TelegramUserDataClass = new TelegramUserDataClass(req.body)
|
||||
}
|
||||
catch (_) {
|
||||
return res.status(422).json({ error: "Malformed data" })
|
||||
}
|
||||
|
||||
if (!userData.isRecent(hashExpirationMs)) {
|
||||
return res.status(408).json({ error: "Telegram login data is not recent" })
|
||||
}
|
||||
|
||||
if (!userData.isValid(botToken)) {
|
||||
return res.status(401).json({ error: "Telegram login data has been tampered" })
|
||||
}
|
||||
|
||||
const accountTelegram = await prisma.accountTelegram.upsert({
|
||||
where: {
|
||||
telegramId: userData.id
|
||||
},
|
||||
create: {
|
||||
...userData.toDatabase(),
|
||||
user: {
|
||||
create: {
|
||||
displayName: userData.toTelegramName(),
|
||||
displayAvatarURL: userData.photoUrl,
|
||||
}
|
||||
}
|
||||
},
|
||||
update: {
|
||||
...userData.toDatabase(),
|
||||
user: {
|
||||
update: {}
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
const token = await prisma.token.create({
|
||||
data: {
|
||||
userId: accountTelegram.userId,
|
||||
token: cryptoRandomString({ length: 16, type: "base64" }),
|
||||
expiresAt: new Date(userData.authDate.getTime() + tokenExpirationMs)
|
||||
},
|
||||
include: {
|
||||
user: true,
|
||||
}
|
||||
})
|
||||
|
||||
return res.status(200).json(token)
|
||||
}
|
|
@ -1,79 +0,0 @@
|
|||
import { NextApiRequest, NextApiResponse } from "next"
|
||||
import { prisma } from "../../../utils/prismaClient"
|
||||
import * as Telegram from "../../../utils/telegram"
|
||||
import {default as cryptoRandomString} from "crypto-random-string"
|
||||
|
||||
|
||||
|
||||
export default function handler(req: NextApiRequest, res: NextApiResponse) {
|
||||
switch(req.method) {
|
||||
case "POST":
|
||||
const token = process.env.TELEGRAM_TOKEN
|
||||
const validity_ms = parseInt(process.env.FESTA_TOKEN_VALIDITY_MS!) // Wrong typing?
|
||||
const now = new Date()
|
||||
|
||||
if(!token) {
|
||||
return res.status(503).json({error: "`TELEGRAM_TOKEN` was not set up"})
|
||||
}
|
||||
if(!validity_ms) {
|
||||
return res.status(503).json({error: "`FESTA_TOKEN_VALIDITY_MS` was not set up"})
|
||||
}
|
||||
|
||||
try {
|
||||
var lr: Telegram.LoginResponse = new Telegram.LoginResponse(req.body)
|
||||
}
|
||||
catch(_) {
|
||||
return res.status(422).json({error: "Malformed data"})
|
||||
}
|
||||
|
||||
if(!lr.isRecent()) {
|
||||
// Not sure?
|
||||
return res.status(408).json({error: "Telegram login data is not recent"})
|
||||
}
|
||||
|
||||
if(!lr.isValid(token)) {
|
||||
return res.status(401).json({error: "Telegram login data has been tampered"})
|
||||
}
|
||||
|
||||
prisma.user.upsert({
|
||||
where: {
|
||||
id: lr.id,
|
||||
},
|
||||
update: {
|
||||
id: lr.id,
|
||||
firstName: lr.first_name,
|
||||
lastName: lr.last_name,
|
||||
username: lr.username,
|
||||
photoUrl: lr.photo_url,
|
||||
lastAuthDate: now,
|
||||
},
|
||||
create: {
|
||||
id: lr.id,
|
||||
firstName: lr.first_name,
|
||||
lastName: lr.last_name,
|
||||
username: lr.username,
|
||||
photoUrl: lr.photo_url,
|
||||
lastAuthDate: now,
|
||||
}
|
||||
})
|
||||
|
||||
const tokenString = cryptoRandomString({length: 16, type: "base64"})
|
||||
const tokenExpiration = new Date(+ now + validity_ms)
|
||||
|
||||
prisma.token.create({
|
||||
data: {
|
||||
userId: lr.id,
|
||||
token: tokenString,
|
||||
expiresAt: tokenExpiration,
|
||||
}
|
||||
})
|
||||
|
||||
return res.status(200).json({
|
||||
token: tokenString,
|
||||
expiresAt: tokenExpiration.toISOString(),
|
||||
})
|
||||
|
||||
default:
|
||||
return res.status(405).json({error: "Invalid method"})
|
||||
}
|
||||
}
|
|
@ -1,10 +1,13 @@
|
|||
import type { NextPage, NextPageContext } from 'next'
|
||||
import { NextPageContext } from 'next'
|
||||
import { useTranslation } from 'next-i18next'
|
||||
import { serverSideTranslations } from 'next-i18next/serverSideTranslations';
|
||||
import { Intro } from '../components/Intro';
|
||||
import { TutorialTelegramLogin } from '../components/TutorialTelegramLogin';
|
||||
import { useState } from 'react';
|
||||
import { LoginContext } from '../contexts/login';
|
||||
import { useDefinedContext } from '../utils/definedContext';
|
||||
import { ApiError } from '../types/api';
|
||||
import { TelegramLoginButton } from "../components/TelegramLoginButton"
|
||||
import { useTelegramToFestaCallback } from '../hooks/useTelegramToFestaCallback';
|
||||
|
||||
|
||||
export async function getStaticProps(context: NextPageContext) {
|
||||
return {
|
||||
|
@ -14,14 +17,24 @@ export async function getStaticProps(context: NextPageContext) {
|
|||
}
|
||||
}
|
||||
|
||||
const Page: NextPage = () => {
|
||||
|
||||
export default function PageIndex() {
|
||||
const { t } = useTranslation("common")
|
||||
const [login, setLogin] = useDefinedContext(LoginContext)
|
||||
const [error, setError] = useState<ApiError | null | undefined>(null)
|
||||
|
||||
if (!login) {
|
||||
return (
|
||||
const onLogin = useTelegramToFestaCallback(setLogin, setError)
|
||||
|
||||
return (
|
||||
login ?
|
||||
<main className="page-index">
|
||||
<hgroup>
|
||||
<h1>
|
||||
{t("siteTitle")}
|
||||
</h1>
|
||||
</main>
|
||||
:
|
||||
<main id="page-hero" className="page">
|
||||
<hgroup className="hgroup-hero">
|
||||
<h1>
|
||||
{t("siteTitle")}
|
||||
</h1>
|
||||
|
@ -29,25 +42,29 @@ const Page: NextPage = () => {
|
|||
{t("siteSubtitle")}
|
||||
</h2>
|
||||
</hgroup>
|
||||
<div>
|
||||
<TutorialTelegramLogin />
|
||||
</div>
|
||||
{
|
||||
error ?
|
||||
<div className="negative">
|
||||
<p>
|
||||
{t("telegramLoginError")}
|
||||
</p>
|
||||
<p>
|
||||
<code>
|
||||
{JSON.stringify(error)}
|
||||
</code>
|
||||
</p>
|
||||
</div>
|
||||
:
|
||||
<div>
|
||||
<p>
|
||||
{t("telegramLoginDescription")}
|
||||
</p>
|
||||
<TelegramLoginButton
|
||||
dataOnauth={onLogin}
|
||||
botName={process.env.NEXT_PUBLIC_TELEGRAM_USERNAME}
|
||||
/>
|
||||
</div>
|
||||
}
|
||||
</main>
|
||||
)
|
||||
}
|
||||
|
||||
return (
|
||||
<main className="page-index">
|
||||
<hgroup>
|
||||
<h1>
|
||||
{t("siteTitle")}
|
||||
</h1>
|
||||
<h2>
|
||||
{t("siteSubtitle")}
|
||||
</h2>
|
||||
</hgroup>
|
||||
</main>
|
||||
)
|
||||
}
|
||||
|
||||
export default Page
|
||||
|
|
|
@ -1,145 +1,72 @@
|
|||
// This is your Prisma schema file,
|
||||
// learn more about it in the docs: https://pris.ly/d/prisma-schema
|
||||
|
||||
generator client {
|
||||
provider = "prisma-client-js"
|
||||
}
|
||||
// Prisma Schema file
|
||||
// https://pris.ly/d/prisma-schema
|
||||
|
||||
// Use the PostgreSQL database at the URL specified via the DATABASE_URL environment variable.
|
||||
datasource db {
|
||||
provider = "postgresql"
|
||||
url = env("DATABASE_URL")
|
||||
}
|
||||
|
||||
/// An event is the representation of a gathering of people in a certain place at a certain time.
|
||||
model Event {
|
||||
id Int @id @default(autoincrement())
|
||||
//
|
||||
slug String
|
||||
createdAt DateTime @default(now())
|
||||
updatedAt DateTime @updatedAt
|
||||
viewPassword String?
|
||||
joinPassword String?
|
||||
//
|
||||
name String
|
||||
description String
|
||||
postcard String?
|
||||
startTime DateTime?
|
||||
endTime DateTime?
|
||||
location String?
|
||||
//
|
||||
partecipants Partecipant[]
|
||||
neededItems Item[]
|
||||
vehicles Vehicle[]
|
||||
// Generate @prisma/client for use in JavaScript and TypeScript.
|
||||
generator client {
|
||||
provider = "prisma-client-js"
|
||||
}
|
||||
|
||||
/// A user is a person who is using the Festa website, who logged in via Telegram.
|
||||
/// A person who is using the Festa website, who may have logged in via various account types.
|
||||
model User {
|
||||
id BigInt @id
|
||||
firstName String
|
||||
lastName String?
|
||||
username String?
|
||||
photoUrl String?
|
||||
lastAuthDate DateTime
|
||||
tokens Token[]
|
||||
/// A unique id for the user on the Festa website.
|
||||
id String @id @default(uuid()) @db.Uuid
|
||||
/// The power level of the user on the Festa website.
|
||||
powerLevel PowerLevel @default(USER)
|
||||
/// The displayed name of the user.
|
||||
displayName String
|
||||
/// The URL of the displayed avatar of the user.
|
||||
displayAvatarURL String?
|
||||
/// The tokens the user can use to login.
|
||||
tokens Token[]
|
||||
/// The Telegram accounts associated with this user.
|
||||
accountsTelegram AccountTelegram[]
|
||||
}
|
||||
|
||||
/// A possible powerLevel value for an {@link User}.
|
||||
enum PowerLevel {
|
||||
/// The user has no special privileges.
|
||||
USER
|
||||
/// The user can override any permission check.
|
||||
SUPERUSER
|
||||
}
|
||||
|
||||
/// A container for user data associated with a single [Telegram](https://telegram.org/).
|
||||
model AccountTelegram {
|
||||
/// The id of the {@link User} associated with this account.
|
||||
userId String @db.Uuid
|
||||
/// The {@link User} associated with this account.
|
||||
user User @relation(fields: [userId], references: [id])
|
||||
/// The Telegram id of the account.
|
||||
telegramId Int @id
|
||||
/// The Telegram first name of the account. Always present.
|
||||
firstName String
|
||||
/// The Telegram last name of the account. May be omitted.
|
||||
lastName String?
|
||||
/// The username of the account. May not be present if the account has not opted in to public discovery on Telegram.
|
||||
/// If set, allows the user to be contacted via `https://t.me/USERNAME`.
|
||||
username String?
|
||||
/// The URL where the user's avatar is accessible at.
|
||||
photoUrl String?
|
||||
/// The locale of the user. Its presence is VERY inconsistent, don't make any assumption based on that.
|
||||
lang String?
|
||||
/// The last time the account was updated.
|
||||
updatedAt DateTime @updatedAt
|
||||
}
|
||||
|
||||
/// A token that can be used to authenticate to the API as an {@link User}.
|
||||
model Token {
|
||||
userId BigInt
|
||||
/// The id of the user that the token allows to login as.
|
||||
userId String @db.Uuid
|
||||
/// The user that the token allows to login as.
|
||||
user User @relation(fields: [userId], references: [id])
|
||||
/// The token itself, a string.
|
||||
token String @id
|
||||
/// The datetime after which the token should cease to be valid for authentication.
|
||||
expiresAt DateTime
|
||||
}
|
||||
|
||||
/// A partecipant is a person who may or may not partecipate to the event.
|
||||
model Partecipant {
|
||||
id Int @id @default(autoincrement())
|
||||
eventId Int
|
||||
event Event @relation(fields: [eventId], references: [id])
|
||||
//
|
||||
name String
|
||||
email String
|
||||
//
|
||||
means PartecipationMeans
|
||||
createdAt DateTime @default(now())
|
||||
joinedAt DateTime?
|
||||
//
|
||||
answer PartecipationAnswer
|
||||
shouldBring Item[]
|
||||
drives Vehicle[] @relation("VehicleDrive")
|
||||
rides Vehicle[] @relation("VehicleRide")
|
||||
expenses Transaction[] @relation("TransactionFrom")
|
||||
income Transaction[] @relation("TransactionTo")
|
||||
}
|
||||
|
||||
enum PartecipationMeans {
|
||||
CREATOR
|
||||
INVITED
|
||||
ACCEPTED
|
||||
JOINED
|
||||
}
|
||||
|
||||
enum PartecipationAnswer {
|
||||
HOST
|
||||
YES
|
||||
MAYBE
|
||||
NO
|
||||
PENDING
|
||||
}
|
||||
|
||||
/// An item which should be bought and brought by somebody to the event.
|
||||
model Item {
|
||||
id Int @id @default(autoincrement())
|
||||
eventId Int
|
||||
event Event @relation(fields: [eventId], references: [id])
|
||||
//
|
||||
quantity Int
|
||||
name String
|
||||
purchased Boolean @default(false)
|
||||
//
|
||||
assignedId Int?
|
||||
assigned Partecipant? @relation(fields: [assignedId], references: [id])
|
||||
}
|
||||
|
||||
/// A vehicle which is being used to transport people from and to the event.
|
||||
model Vehicle {
|
||||
id Int @id @default(autoincrement())
|
||||
eventId Int
|
||||
event Event @relation(fields: [eventId], references: [id])
|
||||
//
|
||||
driverId Int
|
||||
driver Partecipant @relation("VehicleDrive", fields: [driverId], references: [id])
|
||||
riders Partecipant[] @relation("VehicleRide")
|
||||
//
|
||||
type VehicleType @default(CAR)
|
||||
slots Int @default(4)
|
||||
password String?
|
||||
//
|
||||
voyage VoyageType
|
||||
location String
|
||||
departureAt DateTime
|
||||
arrivalAt DateTime
|
||||
}
|
||||
|
||||
enum VehicleType {
|
||||
CAR
|
||||
OTHER
|
||||
}
|
||||
|
||||
enum VoyageType {
|
||||
TO
|
||||
FROM
|
||||
}
|
||||
|
||||
/// A monetary transaction related to the event.
|
||||
model Transaction {
|
||||
id Int @id @default(autoincrement())
|
||||
//
|
||||
fromId Int?
|
||||
from Partecipant? @relation("TransactionFrom", fields: [fromId], references: [id])
|
||||
toId Int?
|
||||
to Partecipant? @relation("TransactionTo", fields: [toId], references: [id])
|
||||
//
|
||||
amount Decimal
|
||||
currency String
|
||||
reason String
|
||||
}
|
||||
|
|
|
@ -1,9 +1,6 @@
|
|||
{
|
||||
"siteTitle": "Festa",
|
||||
"siteSubtitle": "Organizza con facilità il tuo evento!",
|
||||
"introTelegramLogin": "Per prima cosa, effettua il login con Telegram.",
|
||||
"introTelegramLoggedIn": "Sei connesso come <1/>!",
|
||||
"introTelegramLogout": "Non sei tu?",
|
||||
"introCreateEvent": "Dai un nome al tuo primo evento:",
|
||||
"introCreateEventSlugPlaceholder": "nome-evento-2022"
|
||||
"telegramLoginDescription": "Per iniziare, effettua il login con Telegram.",
|
||||
"logOutPrompt": "Non sei tu?"
|
||||
}
|
||||
|
|
|
@ -14,6 +14,10 @@
|
|||
--negative: #880000;
|
||||
}
|
||||
|
||||
.postcard {
|
||||
filter: blur(16px) contrast(25%) brightness(175%);
|
||||
}
|
||||
|
||||
/* Dark theme */
|
||||
@media (prefers-color-scheme: dark) {
|
||||
body {
|
||||
|
@ -30,4 +34,8 @@
|
|||
--positive: #88ff88;
|
||||
--negative: #ff8888;
|
||||
}
|
||||
|
||||
.postcard {
|
||||
filter: blur(16px) contrast(50%) brightness(50%);
|
||||
}
|
||||
}
|
|
@ -1,3 +1,6 @@
|
|||
@import "color-schemes.css";
|
||||
@import "page-tweaks.css";
|
||||
|
||||
* {
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
@ -17,10 +20,15 @@ body {
|
|||
color: var(--foreground);
|
||||
font-family: sans-serif;
|
||||
text-shadow: 1px 1px 1px var(--background);
|
||||
|
||||
min-height: 100vh;
|
||||
}
|
||||
|
||||
hgroup > * {
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
h1, h2, h3, h4, h5, h6 {
|
||||
margin: 0;
|
||||
text-shadow: 2px 2px 2px var(--background);
|
||||
}
|
||||
|
||||
|
@ -36,6 +44,14 @@ a:active {
|
|||
color: var(--anchor-active);
|
||||
}
|
||||
|
||||
.positive {
|
||||
color: var(--positive);
|
||||
}
|
||||
|
||||
.negative {
|
||||
color: var(--negative);
|
||||
}
|
||||
|
||||
input, button {
|
||||
padding: 8px;
|
||||
margin: 2px 4px;
|
||||
|
@ -65,22 +81,35 @@ input[type="submit"]:active, button:active {
|
|||
border-style: inset;
|
||||
}
|
||||
|
||||
.input-square {
|
||||
.square-40 {
|
||||
width: 40px;
|
||||
height: 40px;
|
||||
}
|
||||
|
||||
.input-positive {
|
||||
input.positive, button.positive {
|
||||
border-color: var(--positive);
|
||||
color: var(--positive);
|
||||
}
|
||||
|
||||
.input-negative {
|
||||
input.negative, button.negative {
|
||||
border-color: var(--negative);
|
||||
color: var(--negative);
|
||||
}
|
||||
|
||||
@import "index.css";
|
||||
@import "postcard.css";
|
||||
@import "telegram.css";
|
||||
@import "variables.css";
|
||||
.page {
|
||||
min-height: 100vh;
|
||||
}
|
||||
|
||||
.container-btn-telegram > div {
|
||||
height: 40px;
|
||||
}
|
||||
|
||||
.postcard {
|
||||
width: 100vw;
|
||||
height: 100vh;
|
||||
object-fit: cover;
|
||||
|
||||
position: absolute;
|
||||
z-index: -1;
|
||||
|
||||
user-select: none;
|
||||
pointer-events: none;
|
||||
}
|
||||
|
|
|
@ -1,30 +0,0 @@
|
|||
.page-index {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
|
||||
justify-content: space-evenly;
|
||||
align-items: center;
|
||||
text-align: center;
|
||||
|
||||
min-height: 100vh;
|
||||
}
|
||||
|
||||
@media only screen and (max-width: 639px) {
|
||||
.page-index h1 {
|
||||
font-size: 5rem;
|
||||
}
|
||||
|
||||
.page-index h2 {
|
||||
font-size: 1.5rem;
|
||||
}
|
||||
}
|
||||
|
||||
@media only screen and (min-width: 640px) {
|
||||
.page-index h1 {
|
||||
font-size: 10rem;
|
||||
}
|
||||
|
||||
.page-index h2 {
|
||||
font-size: 2.5rem;
|
||||
}
|
||||
}
|
44
styles/page-tweaks.css
Normal file
44
styles/page-tweaks.css
Normal file
|
@ -0,0 +1,44 @@
|
|||
#page-hero {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
|
||||
justify-content: space-evenly;
|
||||
align-items: center;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
#page-hero h1 {
|
||||
font-size: 10rem;
|
||||
}
|
||||
|
||||
#page-hero h2 {
|
||||
font-size: 2.5rem;
|
||||
}
|
||||
|
||||
@media (max-width: 640px) {
|
||||
#page-hero h1 {
|
||||
font-size: 5rem;
|
||||
}
|
||||
|
||||
#page-hero h2 {
|
||||
font-size: 1.5rem;
|
||||
}
|
||||
}
|
||||
|
||||
#page-events {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
|
||||
justify-content: space-evenly;
|
||||
align-items: center;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
#page-create {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
|
||||
justify-content: space-evenly;
|
||||
align-items: center;
|
||||
text-align: center;
|
||||
}
|
|
@ -1,23 +0,0 @@
|
|||
.postcard {
|
||||
width: 100vw;
|
||||
height: 100vh;
|
||||
object-fit: cover;
|
||||
|
||||
position: absolute;
|
||||
z-index: -1;
|
||||
|
||||
user-select: none;
|
||||
pointer-events: none;
|
||||
}
|
||||
|
||||
@media (prefers-color-scheme: light) {
|
||||
.postcard {
|
||||
filter: blur(16px) contrast(25%) brightness(175%);
|
||||
}
|
||||
}
|
||||
|
||||
@media (prefers-color-scheme: dark) {
|
||||
.postcard {
|
||||
filter: blur(16px) contrast(50%) brightness(50%);
|
||||
}
|
||||
}
|
|
@ -1,3 +0,0 @@
|
|||
.container-btn-telegram > div {
|
||||
height: 40px;
|
||||
}
|
5
types/api.ts
Normal file
5
types/api.ts
Normal file
|
@ -0,0 +1,5 @@
|
|||
export type ApiError = {
|
||||
error: string
|
||||
}
|
||||
|
||||
export type ApiResult<T> = ApiError | T
|
20
types/user.ts
Normal file
20
types/user.ts
Normal file
|
@ -0,0 +1,20 @@
|
|||
import { Token, User } from "@prisma/client"
|
||||
|
||||
/**
|
||||
* Serializable Telegram login data with technical information.
|
||||
*/
|
||||
export type TelegramLoginData = {
|
||||
id: number
|
||||
first_name: string
|
||||
last_name?: string
|
||||
username?: string
|
||||
photo_url?: string
|
||||
lang?: string
|
||||
auth_date: number
|
||||
hash: string
|
||||
}
|
||||
|
||||
/**
|
||||
* Login data for a specific Festa user.
|
||||
*/
|
||||
export type FestaLoginData = Token & {user: User}
|
142
utils/TelegramUserDataClass.ts
Normal file
142
utils/TelegramUserDataClass.ts
Normal file
|
@ -0,0 +1,142 @@
|
|||
import { prisma } from "./prismaClient"
|
||||
import { AccountTelegram, Token, User } from "@prisma/client"
|
||||
import nodecrypto from "crypto"
|
||||
import { TelegramLoginData } from "../types/user"
|
||||
|
||||
|
||||
/**
|
||||
* A {@link TelegramLoginData} object extended with various utility methods.
|
||||
*/
|
||||
export class TelegramUserDataClass {
|
||||
id: number
|
||||
firstName: string
|
||||
lastName?: string
|
||||
username?: string
|
||||
photoUrl?: string
|
||||
authDate: Date
|
||||
hash: string
|
||||
lang?: string
|
||||
|
||||
/**
|
||||
* Construct a {@link TelegramUserDataClass} object from a {@link TelegramLoginData}, validating it in the process.
|
||||
*
|
||||
* @param u The {@link TelegramLoginData} to use.
|
||||
*/
|
||||
constructor(u: TelegramLoginData) {
|
||||
if(!u.id) throw new Error("Missing `id`")
|
||||
if(!u.first_name) throw new Error("Missing `first_name`")
|
||||
if(!u.auth_date) throw new Error("Missing `auth_date`")
|
||||
if(!u.hash) throw new Error("Missing `hash`")
|
||||
|
||||
this.id = u.id
|
||||
this.firstName = u.first_name
|
||||
this.lastName = u.last_name
|
||||
this.username = u.username
|
||||
this.photoUrl = u.photo_url
|
||||
this.authDate = new Date(u.auth_date * 1000)
|
||||
|
||||
// https://stackoverflow.com/a/12372720/4334568
|
||||
if(isNaN(this.authDate.getTime())) throw new Error("Invalid `auth_date`")
|
||||
|
||||
this.hash = u.hash
|
||||
this.lang = u.lang
|
||||
}
|
||||
|
||||
/**
|
||||
* Convert this object back into a {@link TelegramLoginData}.
|
||||
*
|
||||
* @return The {@link TelegramLoginData} object, ready to be serialized.
|
||||
*/
|
||||
toObject(): TelegramLoginData {
|
||||
return {
|
||||
id: this.id,
|
||||
first_name: this.firstName,
|
||||
last_name: this.lastName,
|
||||
username: this.username,
|
||||
photo_url: this.photoUrl,
|
||||
lang: this.lang,
|
||||
auth_date: this.authDate.getTime() / 1000,
|
||||
hash: this.hash,
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Convert this object into a partial {@link AccountTelegram} database object.
|
||||
*/
|
||||
toDatabase() {
|
||||
return {
|
||||
telegramId: this.id,
|
||||
firstName: this.firstName,
|
||||
lastName: this.lastName ?? null,
|
||||
username: this.username ?? null,
|
||||
photoUrl: this.photoUrl ?? null,
|
||||
lang: this.lang ?? null,
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Convert this object in a string, using [the format required to verify a Telegram Login](https://core.telegram.org/widgets/login#checking-authorization).
|
||||
*
|
||||
* @param data The data to encode.
|
||||
* @returns The stringified data.
|
||||
*/
|
||||
toString(): string {
|
||||
const string = Object.entries(this.toObject())
|
||||
.filter(([key, _]) => key !== "hash")
|
||||
.filter(([_, value]) => value !== undefined)
|
||||
.map(([key, value]) => `${key}=${value}`)
|
||||
.sort()
|
||||
.join("\n")
|
||||
return string
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if the `auth_date` of the response is recent: it must be in the past, but within `maxMs` from the current date.
|
||||
*
|
||||
* @param maxMs The maximum number of milliseconds that may pass after authentication for the response to be considered valid.
|
||||
* @returns `true` if the request was sent within the requested timeframe, `false` otherwise.
|
||||
*/
|
||||
isRecent(maxMs: number): boolean {
|
||||
const diff = new Date().getTime() - this.authDate.getTime()
|
||||
return diff <= maxMs
|
||||
}
|
||||
|
||||
/**
|
||||
* Calculate the "`hash`" of this object using [the Telegram Login verification procedure](https://core.telegram.org/widgets/login#checking-authorization).
|
||||
*
|
||||
* _Only works on Node.js, due to usage of the `crypto` module._
|
||||
*
|
||||
* @param token The bot token used to validate the signature.
|
||||
* @returns The calculated value of the `hash` parameter.
|
||||
*/
|
||||
hmac(token: string): string {
|
||||
const hash = nodecrypto.createHash("sha256")
|
||||
hash.update(token)
|
||||
const hmac = nodecrypto.createHmac("sha256", hash.digest())
|
||||
hmac.update(this.toString())
|
||||
return hmac.digest("hex")
|
||||
}
|
||||
|
||||
/**
|
||||
* Validate this object using [the Telegram Login verification procedure](https://core.telegram.org/widgets/login#checking-authorization).
|
||||
*
|
||||
* _Only works on Node.js, due to usage of the `crypto` module._
|
||||
*
|
||||
* @param token The bot token used to validate the signature.
|
||||
* @returns `true` if the hash matches the value calculated with {@link hmac}, `false` otherwise.
|
||||
*/
|
||||
isValid(token: string): boolean {
|
||||
const received = this.hash
|
||||
const calculated = this.hmac(token)
|
||||
return received === calculated
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the Telegram "displayed name" of the user represented by this object.
|
||||
*/
|
||||
toTelegramName(): string {
|
||||
if(this.username) return this.username
|
||||
else if(this.lastName) return this.firstName + " " + this.lastName
|
||||
else return this.firstName
|
||||
}
|
||||
}
|
|
@ -1,148 +0,0 @@
|
|||
import nodecrypto from "crypto"
|
||||
import { ParsedUrlQuery } from "querystring"
|
||||
import * as QueryString from "./queryString"
|
||||
|
||||
/**
|
||||
* Serializable Telegram user data without any technical information.
|
||||
*/
|
||||
export interface UserData {
|
||||
id: number
|
||||
first_name: string
|
||||
last_name?: string
|
||||
username?: string
|
||||
photo_url?: string
|
||||
lang?: string
|
||||
}
|
||||
|
||||
/**
|
||||
* Serializable Telegram login data with technical information.
|
||||
*
|
||||
* Can be turned in a {@link LoginResponse} for additional methods.
|
||||
*/
|
||||
export interface LoginData extends UserData {
|
||||
auth_date: number
|
||||
hash: string
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Get the default Telegram representation of a username.
|
||||
*
|
||||
* @param u
|
||||
* @returns
|
||||
*/
|
||||
export function getTelegramName(u: UserData) {
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Create a {@link LoginData} object from a {@link ParsedUrlQuery}.
|
||||
*
|
||||
* @param queryObj The source object.
|
||||
* @returns The created object.
|
||||
*/
|
||||
export function queryStringToLoginData(queryObj: ParsedUrlQuery): LoginData {
|
||||
return {
|
||||
id: parseInt(QueryString.getSingle(queryObj, "id")),
|
||||
first_name: QueryString.getSingle(queryObj, "first_name"),
|
||||
last_name: QueryString.getSingle(queryObj, "last_name"),
|
||||
username: QueryString.getSingle(queryObj, "username"),
|
||||
photo_url: QueryString.getSingle(queryObj, "photo_url"),
|
||||
lang: QueryString.getSingle(queryObj, "lang"),
|
||||
auth_date: parseInt(QueryString.getSingle(queryObj, "auth_date")),
|
||||
hash: QueryString.getSingle(queryObj, "hash"),
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* The response sent by Telegram after a login.
|
||||
*/
|
||||
export class LoginResponse implements LoginData {
|
||||
id: number
|
||||
first_name: string
|
||||
last_name?: string
|
||||
username?: string
|
||||
photo_url?: string
|
||||
auth_date: number
|
||||
hash: string
|
||||
lang?: string
|
||||
|
||||
/**
|
||||
* Construct a new {@link LoginResponse} from a query string object as returned by Next.js.
|
||||
*
|
||||
* @param queryObj The query string object, from `context.query`.
|
||||
*/
|
||||
constructor(ld: LoginData) {
|
||||
if(!ld.id) throw new Error("Missing `id`")
|
||||
if(!ld.first_name) throw new Error("Missing `first_name`")
|
||||
if(!ld.auth_date) throw new Error("Missing `auth_date`")
|
||||
if(!ld.hash) throw new Error("Missing `hash`")
|
||||
|
||||
this.id = ld.id
|
||||
this.first_name = ld.first_name
|
||||
this.last_name = ld.last_name
|
||||
this.username = ld.username
|
||||
this.photo_url = ld.photo_url
|
||||
this.auth_date = ld.auth_date
|
||||
this.hash = ld.hash
|
||||
this.lang = ld.lang
|
||||
}
|
||||
|
||||
/**
|
||||
* Stringify a {@link LoginResponse} in [the format required to verify a Telegram Login](https://core.telegram.org/widgets/login#checking-authorization).
|
||||
*
|
||||
* @param data The data to encode.
|
||||
* @returns The stringified data.
|
||||
*/
|
||||
stringify(): string {
|
||||
const string = Object.entries(this)
|
||||
.filter(([key, _]) => key !== "hash")
|
||||
.filter(([_, value]) => value !== undefined)
|
||||
.map(([key, value]) => `${key}=${value}`)
|
||||
.sort()
|
||||
.join("\n")
|
||||
return string
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if the `auth_date` of the response is recent: it must be in the past, but within `maxSeconds` from the current date.
|
||||
*
|
||||
* @param maxSeconds The maximum number of milliseconds that may pass after authentication for the response to be considered valid; defaults to `864_000_000`, 1 day.
|
||||
* @returns `true` if the response can be considered recent, `false` otherwise.
|
||||
*/
|
||||
isRecent(maxSeconds: number = 864_000_000): boolean {
|
||||
const diff = new Date().getTime() - new Date(this.auth_date * 1000).getTime()
|
||||
return 0 < diff && diff <= maxSeconds
|
||||
}
|
||||
|
||||
/**
|
||||
* Calculate the "`hash`" of a Telegram Login using [this procedure](https://core.telegram.org/widgets/login#checking-authorization).
|
||||
*
|
||||
* _Only works on Node.js, due to usage of the `crypto` module._
|
||||
*
|
||||
* @param token The bot token used to validate the signature.
|
||||
* @returns The calculated value of the `hash` {@link LoginResponse} parameter.
|
||||
*/
|
||||
hmac(token: string): string {
|
||||
const hash = nodecrypto.createHash("sha256")
|
||||
hash.update(token)
|
||||
const hmac = nodecrypto.createHmac("sha256", hash.digest())
|
||||
hmac.update(this.stringify())
|
||||
return hmac.digest("hex")
|
||||
}
|
||||
|
||||
/**
|
||||
* Validate a Telegram Login using [this procedure](https://core.telegram.org/widgets/login#checking-authorization).
|
||||
*
|
||||
* _Only works on Node.js, due to usage of the `crypto` module._
|
||||
*
|
||||
* @param token The bot token used to validate the signature.
|
||||
* @returns `true` if the validation is successful, `false` otherwise.
|
||||
*/
|
||||
isValid(token: string): boolean {
|
||||
const client = this.hmac(token)
|
||||
const server = this.hash
|
||||
return client === server
|
||||
}
|
||||
}
|
Loading…
Reference in a new issue