mirror of
https://github.com/Steffo99/festa.git
synced 2024-12-22 14:44:21 +00:00
I actually made a lot of stuff without committing
This commit is contained in:
parent
79063b6735
commit
fb96441289
11 changed files with 262 additions and 139 deletions
7
.env
7
.env
|
@ -1,7 +0,0 @@
|
||||||
# Environment variables declared in this file are automatically made available to Prisma.
|
|
||||||
# See the documentation for more detail: https://pris.ly/d/prisma-schema#accessing-environment-variables-from-the-schema
|
|
||||||
|
|
||||||
# Prisma supports the native connection string format for PostgreSQL, MySQL, SQLite, SQL Server, MongoDB and CockroachDB (Preview).
|
|
||||||
# See the documentation for all the connection string options: https://pris.ly/d/connection-strings
|
|
||||||
|
|
||||||
DATABASE_URL="postgresql://steffo@localhost/festa?host=/var/run/postgresql"
|
|
15
.vscode/settings.json
vendored
Normal file
15
.vscode/settings.json
vendored
Normal file
|
@ -0,0 +1,15 @@
|
||||||
|
{
|
||||||
|
"files.exclude": {
|
||||||
|
// Version control
|
||||||
|
"**/.git": true,
|
||||||
|
"**/.svn": true,
|
||||||
|
"**/.hg": true,
|
||||||
|
"**/CVS": true,
|
||||||
|
"**/.DS_Store": true,
|
||||||
|
"**/Thumbs.db": true,
|
||||||
|
// Node
|
||||||
|
"node_modules": true,
|
||||||
|
".next": true,
|
||||||
|
"yarn.lock": true,
|
||||||
|
}
|
||||||
|
}
|
|
@ -13,7 +13,8 @@
|
||||||
"next": "12.1.6",
|
"next": "12.1.6",
|
||||||
"prisma": "^3.14.0",
|
"prisma": "^3.14.0",
|
||||||
"react": "18.1.0",
|
"react": "18.1.0",
|
||||||
"react-dom": "18.1.0"
|
"react-dom": "18.1.0",
|
||||||
|
"react-telegram-login": "^1.1.2"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@types/node": "17.0.35",
|
"@types/node": "17.0.35",
|
||||||
|
|
|
@ -3,7 +3,7 @@ import type { NextPage } from 'next'
|
||||||
const Page: NextPage = () => {
|
const Page: NextPage = () => {
|
||||||
return (
|
return (
|
||||||
<div>
|
<div>
|
||||||
sus
|
wrong page bro
|
||||||
</div>
|
</div>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
47
pages/party/[slug].tsx
Normal file
47
pages/party/[slug].tsx
Normal file
|
@ -0,0 +1,47 @@
|
||||||
|
import type { NextPage, NextPageContext } from 'next'
|
||||||
|
import { useRouter } from 'next/router'
|
||||||
|
import TelegramLoginButton from 'react-telegram-login'
|
||||||
|
import * as Telegram from "../../utils/telegram"
|
||||||
|
|
||||||
|
interface PageProps {
|
||||||
|
userData: Telegram.LoginData | null
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function getServerSideProps(context: NextPageContext): Promise<{props: PageProps}> {
|
||||||
|
const props: PageProps = {
|
||||||
|
userData: null,
|
||||||
|
}
|
||||||
|
|
||||||
|
const token = process.env.BOT_TOKEN
|
||||||
|
if(token === undefined) {
|
||||||
|
throw new Error("BOT_TOKEN is not set on the server-side, cannot perform login validation.")
|
||||||
|
}
|
||||||
|
|
||||||
|
if(context.query.hash !== undefined) {
|
||||||
|
const loginResponse = new Telegram.LoginResponse(context.query)
|
||||||
|
if(loginResponse.isRecent() && loginResponse.isValid(process.env.BOT_TOKEN)) {
|
||||||
|
props.userData = loginResponse.serialize()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return {props}
|
||||||
|
}
|
||||||
|
|
||||||
|
const Page: NextPage<PageProps> = ({userData}) => {
|
||||||
|
const router = useRouter()
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div>
|
||||||
|
{userData ?
|
||||||
|
JSON.stringify(userData)
|
||||||
|
:
|
||||||
|
<TelegramLoginButton
|
||||||
|
dataAuthUrl={router.asPath}
|
||||||
|
botName="festaappbot"
|
||||||
|
/>
|
||||||
|
}
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
export default Page
|
|
@ -32,6 +32,17 @@ model Event {
|
||||||
vehicles Vehicle[]
|
vehicles Vehicle[]
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// A user is a person who is using the Festa website, who logged in via Telegram.
|
||||||
|
model User {
|
||||||
|
id BigInt @id
|
||||||
|
firstName String
|
||||||
|
lastName String?
|
||||||
|
username String?
|
||||||
|
photoUrl String?
|
||||||
|
lastAuthDate DateTime
|
||||||
|
hash String
|
||||||
|
}
|
||||||
|
|
||||||
/// A partecipant is a person who may or may not partecipate to the event.
|
/// A partecipant is a person who may or may not partecipate to the event.
|
||||||
model Partecipant {
|
model Partecipant {
|
||||||
id Int @id @default(autoincrement())
|
id Int @id @default(autoincrement())
|
||||||
|
|
|
@ -1,116 +0,0 @@
|
||||||
.container {
|
|
||||||
padding: 0 2rem;
|
|
||||||
}
|
|
||||||
|
|
||||||
.main {
|
|
||||||
min-height: 100vh;
|
|
||||||
padding: 4rem 0;
|
|
||||||
flex: 1;
|
|
||||||
display: flex;
|
|
||||||
flex-direction: column;
|
|
||||||
justify-content: center;
|
|
||||||
align-items: center;
|
|
||||||
}
|
|
||||||
|
|
||||||
.footer {
|
|
||||||
display: flex;
|
|
||||||
flex: 1;
|
|
||||||
padding: 2rem 0;
|
|
||||||
border-top: 1px solid #eaeaea;
|
|
||||||
justify-content: center;
|
|
||||||
align-items: center;
|
|
||||||
}
|
|
||||||
|
|
||||||
.footer a {
|
|
||||||
display: flex;
|
|
||||||
justify-content: center;
|
|
||||||
align-items: center;
|
|
||||||
flex-grow: 1;
|
|
||||||
}
|
|
||||||
|
|
||||||
.title a {
|
|
||||||
color: #0070f3;
|
|
||||||
text-decoration: none;
|
|
||||||
}
|
|
||||||
|
|
||||||
.title a:hover,
|
|
||||||
.title a:focus,
|
|
||||||
.title a:active {
|
|
||||||
text-decoration: underline;
|
|
||||||
}
|
|
||||||
|
|
||||||
.title {
|
|
||||||
margin: 0;
|
|
||||||
line-height: 1.15;
|
|
||||||
font-size: 4rem;
|
|
||||||
}
|
|
||||||
|
|
||||||
.title,
|
|
||||||
.description {
|
|
||||||
text-align: center;
|
|
||||||
}
|
|
||||||
|
|
||||||
.description {
|
|
||||||
margin: 4rem 0;
|
|
||||||
line-height: 1.5;
|
|
||||||
font-size: 1.5rem;
|
|
||||||
}
|
|
||||||
|
|
||||||
.code {
|
|
||||||
background: #fafafa;
|
|
||||||
border-radius: 5px;
|
|
||||||
padding: 0.75rem;
|
|
||||||
font-size: 1.1rem;
|
|
||||||
font-family: Menlo, Monaco, Lucida Console, Liberation Mono, DejaVu Sans Mono,
|
|
||||||
Bitstream Vera Sans Mono, Courier New, monospace;
|
|
||||||
}
|
|
||||||
|
|
||||||
.grid {
|
|
||||||
display: flex;
|
|
||||||
align-items: center;
|
|
||||||
justify-content: center;
|
|
||||||
flex-wrap: wrap;
|
|
||||||
max-width: 800px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.card {
|
|
||||||
margin: 1rem;
|
|
||||||
padding: 1.5rem;
|
|
||||||
text-align: left;
|
|
||||||
color: inherit;
|
|
||||||
text-decoration: none;
|
|
||||||
border: 1px solid #eaeaea;
|
|
||||||
border-radius: 10px;
|
|
||||||
transition: color 0.15s ease, border-color 0.15s ease;
|
|
||||||
max-width: 300px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.card:hover,
|
|
||||||
.card:focus,
|
|
||||||
.card:active {
|
|
||||||
color: #0070f3;
|
|
||||||
border-color: #0070f3;
|
|
||||||
}
|
|
||||||
|
|
||||||
.card h2 {
|
|
||||||
margin: 0 0 1rem 0;
|
|
||||||
font-size: 1.5rem;
|
|
||||||
}
|
|
||||||
|
|
||||||
.card p {
|
|
||||||
margin: 0;
|
|
||||||
font-size: 1.25rem;
|
|
||||||
line-height: 1.5;
|
|
||||||
}
|
|
||||||
|
|
||||||
.logo {
|
|
||||||
height: 1em;
|
|
||||||
margin-left: 0.5rem;
|
|
||||||
}
|
|
||||||
|
|
||||||
@media (max-width: 600px) {
|
|
||||||
.grid {
|
|
||||||
width: 100%;
|
|
||||||
flex-direction: column;
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -1,16 +1,22 @@
|
||||||
html,
|
html {
|
||||||
body {
|
padding: 0;
|
||||||
padding: 0;
|
margin: 0;
|
||||||
margin: 0;
|
|
||||||
font-family: -apple-system, BlinkMacSystemFont, Segoe UI, Roboto, Oxygen,
|
|
||||||
Ubuntu, Cantarell, Fira Sans, Droid Sans, Helvetica Neue, sans-serif;
|
|
||||||
}
|
|
||||||
|
|
||||||
a {
|
|
||||||
color: inherit;
|
|
||||||
text-decoration: none;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
* {
|
* {
|
||||||
box-sizing: border-box;
|
box-sizing: border-box;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@media (prefers-color-scheme: light) {
|
||||||
|
body {
|
||||||
|
background-color: white;
|
||||||
|
color: black;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@media (prefers-color-scheme: dark) {
|
||||||
|
body {
|
||||||
|
background-color: black;
|
||||||
|
color: white;
|
||||||
|
}
|
||||||
|
}
|
|
@ -13,7 +13,7 @@
|
||||||
"resolveJsonModule": true,
|
"resolveJsonModule": true,
|
||||||
"isolatedModules": true,
|
"isolatedModules": true,
|
||||||
"jsx": "preserve",
|
"jsx": "preserve",
|
||||||
"incremental": true
|
"incremental": true,
|
||||||
},
|
},
|
||||||
"include": ["next-env.d.ts", "**/*.ts", "**/*.tsx"],
|
"include": ["next-env.d.ts", "**/*.ts", "**/*.tsx"],
|
||||||
"exclude": ["node_modules"]
|
"exclude": ["node_modules"]
|
||||||
|
|
150
utils/telegram.ts
Normal file
150
utils/telegram.ts
Normal file
|
@ -0,0 +1,150 @@
|
||||||
|
import nodecrypto from "crypto"
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The validated user data serialized by the server.
|
||||||
|
*/
|
||||||
|
export interface LoginData {
|
||||||
|
id: number
|
||||||
|
first_name: string
|
||||||
|
last_name: string | null
|
||||||
|
username: string | null
|
||||||
|
photo_url: string | null
|
||||||
|
lang: string | null
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The response sent by Telegram after a login.
|
||||||
|
*/
|
||||||
|
export class LoginResponse {
|
||||||
|
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(queryObj: {[_: string]: string | string[]}) {
|
||||||
|
if(typeof queryObj.id === "object") {
|
||||||
|
throw new Error("Multiple `id` parameters specified in the query string, cannot construct LoginResponse.")
|
||||||
|
}
|
||||||
|
if(typeof queryObj.first_name === "object") {
|
||||||
|
throw new Error("Multiple `first_name` parameters specified in the query string, cannot construct LoginResponse.")
|
||||||
|
}
|
||||||
|
if(typeof queryObj.last_name === "object") {
|
||||||
|
throw new Error("Multiple `last_name` parameters specified in the query string, cannot construct LoginResponse.")
|
||||||
|
}
|
||||||
|
if(typeof queryObj.username === "object") {
|
||||||
|
throw new Error("Multiple `username` parameters specified in the query string, cannot construct LoginResponse.")
|
||||||
|
}
|
||||||
|
if(typeof queryObj.photo_url === "object") {
|
||||||
|
throw new Error("Multiple `photo_url` parameters specified in the query string, cannot construct LoginResponse.")
|
||||||
|
}
|
||||||
|
if(typeof queryObj.auth_date === "object") {
|
||||||
|
throw new Error("Multiple `auth_date` parameters specified in the query string, cannot construct LoginResponse.")
|
||||||
|
}
|
||||||
|
if(typeof queryObj.hash === "object") {
|
||||||
|
throw new Error("Multiple `hash` parameters specified in the query string, cannot construct LoginResponse.")
|
||||||
|
}
|
||||||
|
if(typeof queryObj.lang === "object") {
|
||||||
|
throw new Error("Multiple `hash` parameters specified in the query string, cannot construct LoginResponse.")
|
||||||
|
}
|
||||||
|
|
||||||
|
this.id = parseInt(queryObj.id)
|
||||||
|
this.first_name = queryObj.first_name
|
||||||
|
this.last_name = queryObj.last_name
|
||||||
|
this.username = queryObj.username
|
||||||
|
this.photo_url = queryObj.photo_url
|
||||||
|
this.auth_date = parseInt(queryObj.auth_date)
|
||||||
|
this.hash = queryObj.hash
|
||||||
|
this.lang = queryObj.lang
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Serialize this response into a {@link LoginData} object, which can be passed to the client by Next.js.
|
||||||
|
*
|
||||||
|
* @returns The {@link LoginData} object.
|
||||||
|
*/
|
||||||
|
serialize(): LoginData {
|
||||||
|
return {
|
||||||
|
id: this.id ?? null,
|
||||||
|
first_name: this.first_name ?? null,
|
||||||
|
last_name: this.last_name ?? null,
|
||||||
|
username: this.username ?? null,
|
||||||
|
photo_url: this.photo_url ?? null,
|
||||||
|
lang: this.lang ?? null,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 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 `300000`, 5 minutes.
|
||||||
|
* @returns `true` if the response can be considered recent, `false` otherwise.
|
||||||
|
*/
|
||||||
|
isRecent(maxSeconds: number = 300000): 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).
|
||||||
|
*
|
||||||
|
* @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 key = hashToken(token)
|
||||||
|
const hmac = nodecrypto.createHmac("sha256", key)
|
||||||
|
hmac.update(this.stringify())
|
||||||
|
return hmac.digest("hex")
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Validate a Telegram Login using [this procedure](https://core.telegram.org/widgets/login#checking-authorization).
|
||||||
|
*
|
||||||
|
* @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
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Hash a Telegram bot token using SHA-256.
|
||||||
|
*
|
||||||
|
* @param token The bot token to hash.
|
||||||
|
* @returns The hex digest of the hash.
|
||||||
|
*/
|
||||||
|
function hashToken(token: string): Buffer {
|
||||||
|
const hash = nodecrypto.createHash("sha256")
|
||||||
|
hash.update(token)
|
||||||
|
return hash.digest()
|
||||||
|
}
|
18
yarn.lock
18
yarn.lock
|
@ -1406,7 +1406,7 @@ prisma@^3.14.0:
|
||||||
dependencies:
|
dependencies:
|
||||||
"@prisma/engines" "3.14.0-36.2b0c12756921c891fec4f68d9444e18c7d5d4a6a"
|
"@prisma/engines" "3.14.0-36.2b0c12756921c891fec4f68d9444e18c7d5d4a6a"
|
||||||
|
|
||||||
prop-types@^15.8.1:
|
prop-types@^15.6.2, prop-types@^15.8.1:
|
||||||
version "15.8.1"
|
version "15.8.1"
|
||||||
resolved "https://registry.npmjs.org/prop-types/-/prop-types-15.8.1.tgz#67d87bf1a694f48435cf332c24af10214a3140b5"
|
resolved "https://registry.npmjs.org/prop-types/-/prop-types-15.8.1.tgz#67d87bf1a694f48435cf332c24af10214a3140b5"
|
||||||
integrity sha512-oj87CgZICdulUohogVAR7AjlC0327U4el4L6eAvOqCeudMDVU0NThNaV+b9Df4dXgSP1gXMTnPdhfe/2qDH5cg==
|
integrity sha512-oj87CgZICdulUohogVAR7AjlC0327U4el4L6eAvOqCeudMDVU0NThNaV+b9Df4dXgSP1gXMTnPdhfe/2qDH5cg==
|
||||||
|
@ -1438,6 +1438,13 @@ react-is@^16.13.1:
|
||||||
resolved "https://registry.npmjs.org/react-is/-/react-is-16.13.1.tgz#789729a4dc36de2999dc156dd6c1d9c18cea56a4"
|
resolved "https://registry.npmjs.org/react-is/-/react-is-16.13.1.tgz#789729a4dc36de2999dc156dd6c1d9c18cea56a4"
|
||||||
integrity sha512-24e6ynE2H+OKt4kqsOvNd8kBpV65zoxbA4BVsEOB3ARVWQki/DHzaUoC5KuON/BiccDaCCTZBuOcfZs70kR8bQ==
|
integrity sha512-24e6ynE2H+OKt4kqsOvNd8kBpV65zoxbA4BVsEOB3ARVWQki/DHzaUoC5KuON/BiccDaCCTZBuOcfZs70kR8bQ==
|
||||||
|
|
||||||
|
react-telegram-login@^1.1.2:
|
||||||
|
version "1.1.2"
|
||||||
|
resolved "https://registry.npmjs.org/react-telegram-login/-/react-telegram-login-1.1.2.tgz#28b9bdd68bb2710afca19354ac1f9428092836f0"
|
||||||
|
integrity sha512-pDP+bvfaklWgnK5O6yvZnIwgky0nnYUU6Zhk0EjdMSkPsLQoOzZRsXIoZnbxyBXhi7346bsxMH+EwwJPTxClDw==
|
||||||
|
dependencies:
|
||||||
|
react "^16.13.1"
|
||||||
|
|
||||||
react@18.1.0:
|
react@18.1.0:
|
||||||
version "18.1.0"
|
version "18.1.0"
|
||||||
resolved "https://registry.npmjs.org/react/-/react-18.1.0.tgz#6f8620382decb17fdc5cc223a115e2adbf104890"
|
resolved "https://registry.npmjs.org/react/-/react-18.1.0.tgz#6f8620382decb17fdc5cc223a115e2adbf104890"
|
||||||
|
@ -1445,6 +1452,15 @@ react@18.1.0:
|
||||||
dependencies:
|
dependencies:
|
||||||
loose-envify "^1.1.0"
|
loose-envify "^1.1.0"
|
||||||
|
|
||||||
|
react@^16.13.1:
|
||||||
|
version "16.14.0"
|
||||||
|
resolved "https://registry.npmjs.org/react/-/react-16.14.0.tgz#94d776ddd0aaa37da3eda8fc5b6b18a4c9a3114d"
|
||||||
|
integrity sha512-0X2CImDkJGApiAlcf0ODKIneSwBPhqJawOa5wCtKbu7ZECrmS26NvtSILynQ66cgkT/RJ4LidJOc3bUESwmU8g==
|
||||||
|
dependencies:
|
||||||
|
loose-envify "^1.1.0"
|
||||||
|
object-assign "^4.1.1"
|
||||||
|
prop-types "^15.6.2"
|
||||||
|
|
||||||
regenerator-runtime@^0.13.4:
|
regenerator-runtime@^0.13.4:
|
||||||
version "0.13.9"
|
version "0.13.9"
|
||||||
resolved "https://registry.npmjs.org/regenerator-runtime/-/regenerator-runtime-0.13.9.tgz#8925742a98ffd90814988d7566ad30ca3b263b52"
|
resolved "https://registry.npmjs.org/regenerator-runtime/-/regenerator-runtime-0.13.9.tgz#8925742a98ffd90814988d7566ad30ca3b263b52"
|
||||||
|
|
Loading…
Reference in a new issue