Compare commits

..

9 Commits

@ -0,0 +1,4 @@
Dockerfile
node_modules
.git
*.local*

@ -7,19 +7,15 @@
kind: pipeline kind: pipeline
name: default name: default
type: docker
steps: steps:
- name: deploy - name: deploy
image: node:22-alpine image: node:latest
volumes: volumes:
- name: host-website-dist - name: host-website-dist
path: /mnt/website path: /mnt/website
commands: commands:
- uname -a - npm install
- node -v
- npm ci
- node -e 'import Sharp from "sharp"; console.log(Sharp)'
- npm run build - npm run build
- cp -rT ./dist /mnt/website - cp -rT ./dist /mnt/website
@ -36,8 +32,8 @@ trigger:
--- ---
kind: pipeline kind: pipeline
name: caddy-permissions
type: exec # this job is executed on the host machine type: exec # this job is executed on the host machine
name: caddy-permissions
depends_on: depends_on:
- default - default

@ -0,0 +1,11 @@
DB_FILE_NAME=file:data.local/database.db
BETTER_AUTH_SECRET=
BETTER_AUTH_URL=http://localhost:3000
OAUTH_CLIENT_ID=
OAUTH_CLIENT_SECRET=
OAUTH_AUTH_URL=https://iam.unipi.it/oauth2/authorize
OAUTH_TOKEN_HOST=https://iam.unipi.it
OAUTH_TOKEN_PATH=/oauth2/token
OAUTH_REDIRECT_URL=https://phc.dm.unipi.it/api/auth/oauth2/callback/unipi
OAUTH_USER_INFO_URL=https://iam.unipi.it/oauth2/userinfo
OAUTH_SCOPES="openid profile email"

@ -0,0 +1,35 @@
# https://dev.to/code42cate/how-to-dockerize-and-deploy-astro-6ll
# use the official Bun image
# see all versions at https://hub.docker.com/r/oven/bun/tags
FROM oven/bun:1-alpine AS base
WORKDIR /app
RUN apk add --no-cache \
git \
curl \
build-base \
python3
# install dependencies into temp directory to cache them and speed up future builds
FROM base AS install
RUN mkdir -p /temp/dev
COPY package.json bun.lockb /temp/dev/
RUN cd /temp/dev && bun install --frozen-lockfile
RUN mkdir -p /temp/prod
COPY package.json bun.lockb /temp/prod/
RUN cd /temp/prod && bun install --frozen-lockfile --production
# copy node_modules from temp directory and copy (non-ignored) project files into the image
FROM base AS prerelease
COPY --from=install /temp/dev/node_modules node_modules
COPY . .
RUN bun run build
# copy production dependencies and source code into final image
FROM base AS release
COPY --from=install /temp/prod/node_modules node_modules
COPY --from=prerelease /app .
ENV HOST=0.0.0.0
ENV PORT=3000
EXPOSE 3000/tcp
CMD bun run start

@ -6,20 +6,25 @@ import remarkMath from 'remark-math'
import yaml from '@rollup/plugin-yaml' import yaml from '@rollup/plugin-yaml'
import node from '@astrojs/node'
// https://astro.build/config // https://astro.build/config
export default defineConfig({ export default defineConfig({
vite: { vite: {
plugins: [yaml()], plugins: [yaml()],
}, },
server: { server: {
port: 3000, port: 3000,
}, },
markdown: { markdown: {
remarkPlugins: [remarkMath], remarkPlugins: [remarkMath],
shikiConfig: { shikiConfig: {
theme: 'github-light', theme: 'github-light',
}, },
}, },
integrations: [ integrations: [
preact({ preact({
compat: true, compat: true,
@ -28,5 +33,10 @@ export default defineConfig({
remarkPlugins: [remarkMath], remarkPlugins: [remarkMath],
}), }),
], ],
output: 'static',
output: 'server',
adapter: node({
mode: 'standalone',
}),
}) })

Binary file not shown.

@ -0,0 +1,12 @@
services:
website:
build:
context: .
container_name: website
restart: unless-stopped
ports:
# https://gist.github.com/aziis98/88af12b32d9cf3eeae3929b93146fd27
# hash2addr "next.phc.dm.unipi.it"
- "127.44.207.62:1059:3000"
volumes:
- /var/lib/website:/app/data.local

@ -0,0 +1,11 @@
import 'dotenv/config'
import { defineConfig } from 'drizzle-kit'
export default defineConfig({
out: './drizzle',
schema: './src/db/schema.ts',
dialect: 'sqlite',
dbCredentials: {
url: process.env.DB_FILE_NAME!,
},
})

@ -0,0 +1,82 @@
CREATE TABLE `accounts` (
`user_id` text,
`provider` text NOT NULL,
`username` text NOT NULL,
FOREIGN KEY (`user_id`) REFERENCES `user`(`id`) ON UPDATE no action ON DELETE no action
);
--> statement-breakpoint
CREATE TABLE `appunti` (
`user_id` text,
`hash` text NOT NULL,
`link` text,
`visibility` text NOT NULL,
`title` text NOT NULL,
`description` text,
`tags` text NOT NULL,
FOREIGN KEY (`user_id`) REFERENCES `users`(`id`) ON UPDATE no action ON DELETE no action
);
--> statement-breakpoint
CREATE TABLE `poisson_requests` (
`user_id` text,
`request_type` text NOT NULL,
`comments` text,
FOREIGN KEY (`user_id`) REFERENCES `users`(`id`) ON UPDATE no action ON DELETE no action
);
--> statement-breakpoint
CREATE TABLE `users` (
`id` text,
`username` text NOT NULL,
`full_name` text,
`email` text,
FOREIGN KEY (`id`) REFERENCES `user`(`id`) ON UPDATE no action ON DELETE no action
);
--> statement-breakpoint
CREATE TABLE `account` (
`id` text PRIMARY KEY NOT NULL,
`account_id` text NOT NULL,
`provider_id` text NOT NULL,
`user_id` text NOT NULL,
`access_token` text,
`refresh_token` text,
`id_token` text,
`access_token_expires_at` integer,
`refresh_token_expires_at` integer,
`scope` text,
`password` text,
`created_at` integer NOT NULL,
`updated_at` integer NOT NULL,
FOREIGN KEY (`user_id`) REFERENCES `user`(`id`) ON UPDATE no action ON DELETE cascade
);
--> statement-breakpoint
CREATE TABLE `session` (
`id` text PRIMARY KEY NOT NULL,
`expires_at` integer NOT NULL,
`token` text NOT NULL,
`created_at` integer NOT NULL,
`updated_at` integer NOT NULL,
`ip_address` text,
`user_agent` text,
`user_id` text NOT NULL,
FOREIGN KEY (`user_id`) REFERENCES `user`(`id`) ON UPDATE no action ON DELETE cascade
);
--> statement-breakpoint
CREATE UNIQUE INDEX `session_token_unique` ON `session` (`token`);--> statement-breakpoint
CREATE TABLE `user` (
`id` text PRIMARY KEY NOT NULL,
`name` text NOT NULL,
`email` text NOT NULL,
`email_verified` integer NOT NULL,
`image` text,
`created_at` integer NOT NULL,
`updated_at` integer NOT NULL
);
--> statement-breakpoint
CREATE UNIQUE INDEX `user_email_unique` ON `user` (`email`);--> statement-breakpoint
CREATE TABLE `verification` (
`id` text PRIMARY KEY NOT NULL,
`identifier` text NOT NULL,
`value` text NOT NULL,
`expires_at` integer NOT NULL,
`created_at` integer,
`updated_at` integer
);

@ -0,0 +1,555 @@
{
"version": "6",
"dialect": "sqlite",
"id": "84352768-6c24-4412-9d9e-8a3c756d31b1",
"prevId": "00000000-0000-0000-0000-000000000000",
"tables": {
"accounts": {
"name": "accounts",
"columns": {
"user_id": {
"name": "user_id",
"type": "text",
"primaryKey": false,
"notNull": false,
"autoincrement": false
},
"provider": {
"name": "provider",
"type": "text",
"primaryKey": false,
"notNull": true,
"autoincrement": false
},
"username": {
"name": "username",
"type": "text",
"primaryKey": false,
"notNull": true,
"autoincrement": false
}
},
"indexes": {},
"foreignKeys": {
"accounts_user_id_user_id_fk": {
"name": "accounts_user_id_user_id_fk",
"tableFrom": "accounts",
"tableTo": "user",
"columnsFrom": [
"user_id"
],
"columnsTo": [
"id"
],
"onDelete": "no action",
"onUpdate": "no action"
}
},
"compositePrimaryKeys": {},
"uniqueConstraints": {},
"checkConstraints": {}
},
"appunti": {
"name": "appunti",
"columns": {
"user_id": {
"name": "user_id",
"type": "text",
"primaryKey": false,
"notNull": false,
"autoincrement": false
},
"hash": {
"name": "hash",
"type": "text",
"primaryKey": false,
"notNull": true,
"autoincrement": false
},
"link": {
"name": "link",
"type": "text",
"primaryKey": false,
"notNull": false,
"autoincrement": false
},
"visibility": {
"name": "visibility",
"type": "text",
"primaryKey": false,
"notNull": true,
"autoincrement": false
},
"title": {
"name": "title",
"type": "text",
"primaryKey": false,
"notNull": true,
"autoincrement": false
},
"description": {
"name": "description",
"type": "text",
"primaryKey": false,
"notNull": false,
"autoincrement": false
},
"tags": {
"name": "tags",
"type": "text",
"primaryKey": false,
"notNull": true,
"autoincrement": false
}
},
"indexes": {},
"foreignKeys": {
"appunti_user_id_users_id_fk": {
"name": "appunti_user_id_users_id_fk",
"tableFrom": "appunti",
"tableTo": "users",
"columnsFrom": [
"user_id"
],
"columnsTo": [
"id"
],
"onDelete": "no action",
"onUpdate": "no action"
}
},
"compositePrimaryKeys": {},
"uniqueConstraints": {},
"checkConstraints": {}
},
"poisson_requests": {
"name": "poisson_requests",
"columns": {
"user_id": {
"name": "user_id",
"type": "text",
"primaryKey": false,
"notNull": false,
"autoincrement": false
},
"request_type": {
"name": "request_type",
"type": "text",
"primaryKey": false,
"notNull": true,
"autoincrement": false
},
"comments": {
"name": "comments",
"type": "text",
"primaryKey": false,
"notNull": false,
"autoincrement": false
}
},
"indexes": {},
"foreignKeys": {
"poisson_requests_user_id_users_id_fk": {
"name": "poisson_requests_user_id_users_id_fk",
"tableFrom": "poisson_requests",
"tableTo": "users",
"columnsFrom": [
"user_id"
],
"columnsTo": [
"id"
],
"onDelete": "no action",
"onUpdate": "no action"
}
},
"compositePrimaryKeys": {},
"uniqueConstraints": {},
"checkConstraints": {}
},
"users": {
"name": "users",
"columns": {
"id": {
"name": "id",
"type": "text",
"primaryKey": false,
"notNull": false,
"autoincrement": false
},
"username": {
"name": "username",
"type": "text",
"primaryKey": false,
"notNull": true,
"autoincrement": false
},
"full_name": {
"name": "full_name",
"type": "text",
"primaryKey": false,
"notNull": false,
"autoincrement": false
},
"email": {
"name": "email",
"type": "text",
"primaryKey": false,
"notNull": false,
"autoincrement": false
}
},
"indexes": {},
"foreignKeys": {
"users_id_user_id_fk": {
"name": "users_id_user_id_fk",
"tableFrom": "users",
"tableTo": "user",
"columnsFrom": [
"id"
],
"columnsTo": [
"id"
],
"onDelete": "no action",
"onUpdate": "no action"
}
},
"compositePrimaryKeys": {},
"uniqueConstraints": {},
"checkConstraints": {}
},
"account": {
"name": "account",
"columns": {
"id": {
"name": "id",
"type": "text",
"primaryKey": true,
"notNull": true,
"autoincrement": false
},
"account_id": {
"name": "account_id",
"type": "text",
"primaryKey": false,
"notNull": true,
"autoincrement": false
},
"provider_id": {
"name": "provider_id",
"type": "text",
"primaryKey": false,
"notNull": true,
"autoincrement": false
},
"user_id": {
"name": "user_id",
"type": "text",
"primaryKey": false,
"notNull": true,
"autoincrement": false
},
"access_token": {
"name": "access_token",
"type": "text",
"primaryKey": false,
"notNull": false,
"autoincrement": false
},
"refresh_token": {
"name": "refresh_token",
"type": "text",
"primaryKey": false,
"notNull": false,
"autoincrement": false
},
"id_token": {
"name": "id_token",
"type": "text",
"primaryKey": false,
"notNull": false,
"autoincrement": false
},
"access_token_expires_at": {
"name": "access_token_expires_at",
"type": "integer",
"primaryKey": false,
"notNull": false,
"autoincrement": false
},
"refresh_token_expires_at": {
"name": "refresh_token_expires_at",
"type": "integer",
"primaryKey": false,
"notNull": false,
"autoincrement": false
},
"scope": {
"name": "scope",
"type": "text",
"primaryKey": false,
"notNull": false,
"autoincrement": false
},
"password": {
"name": "password",
"type": "text",
"primaryKey": false,
"notNull": false,
"autoincrement": false
},
"created_at": {
"name": "created_at",
"type": "integer",
"primaryKey": false,
"notNull": true,
"autoincrement": false
},
"updated_at": {
"name": "updated_at",
"type": "integer",
"primaryKey": false,
"notNull": true,
"autoincrement": false
}
},
"indexes": {},
"foreignKeys": {
"account_user_id_user_id_fk": {
"name": "account_user_id_user_id_fk",
"tableFrom": "account",
"tableTo": "user",
"columnsFrom": [
"user_id"
],
"columnsTo": [
"id"
],
"onDelete": "cascade",
"onUpdate": "no action"
}
},
"compositePrimaryKeys": {},
"uniqueConstraints": {},
"checkConstraints": {}
},
"session": {
"name": "session",
"columns": {
"id": {
"name": "id",
"type": "text",
"primaryKey": true,
"notNull": true,
"autoincrement": false
},
"expires_at": {
"name": "expires_at",
"type": "integer",
"primaryKey": false,
"notNull": true,
"autoincrement": false
},
"token": {
"name": "token",
"type": "text",
"primaryKey": false,
"notNull": true,
"autoincrement": false
},
"created_at": {
"name": "created_at",
"type": "integer",
"primaryKey": false,
"notNull": true,
"autoincrement": false
},
"updated_at": {
"name": "updated_at",
"type": "integer",
"primaryKey": false,
"notNull": true,
"autoincrement": false
},
"ip_address": {
"name": "ip_address",
"type": "text",
"primaryKey": false,
"notNull": false,
"autoincrement": false
},
"user_agent": {
"name": "user_agent",
"type": "text",
"primaryKey": false,
"notNull": false,
"autoincrement": false
},
"user_id": {
"name": "user_id",
"type": "text",
"primaryKey": false,
"notNull": true,
"autoincrement": false
}
},
"indexes": {
"session_token_unique": {
"name": "session_token_unique",
"columns": [
"token"
],
"isUnique": true
}
},
"foreignKeys": {
"session_user_id_user_id_fk": {
"name": "session_user_id_user_id_fk",
"tableFrom": "session",
"tableTo": "user",
"columnsFrom": [
"user_id"
],
"columnsTo": [
"id"
],
"onDelete": "cascade",
"onUpdate": "no action"
}
},
"compositePrimaryKeys": {},
"uniqueConstraints": {},
"checkConstraints": {}
},
"user": {
"name": "user",
"columns": {
"id": {
"name": "id",
"type": "text",
"primaryKey": true,
"notNull": true,
"autoincrement": false
},
"name": {
"name": "name",
"type": "text",
"primaryKey": false,
"notNull": true,
"autoincrement": false
},
"email": {
"name": "email",
"type": "text",
"primaryKey": false,
"notNull": true,
"autoincrement": false
},
"email_verified": {
"name": "email_verified",
"type": "integer",
"primaryKey": false,
"notNull": true,
"autoincrement": false
},
"image": {
"name": "image",
"type": "text",
"primaryKey": false,
"notNull": false,
"autoincrement": false
},
"created_at": {
"name": "created_at",
"type": "integer",
"primaryKey": false,
"notNull": true,
"autoincrement": false
},
"updated_at": {
"name": "updated_at",
"type": "integer",
"primaryKey": false,
"notNull": true,
"autoincrement": false
}
},
"indexes": {
"user_email_unique": {
"name": "user_email_unique",
"columns": [
"email"
],
"isUnique": true
}
},
"foreignKeys": {},
"compositePrimaryKeys": {},
"uniqueConstraints": {},
"checkConstraints": {}
},
"verification": {
"name": "verification",
"columns": {
"id": {
"name": "id",
"type": "text",
"primaryKey": true,
"notNull": true,
"autoincrement": false
},
"identifier": {
"name": "identifier",
"type": "text",
"primaryKey": false,
"notNull": true,
"autoincrement": false
},
"value": {
"name": "value",
"type": "text",
"primaryKey": false,
"notNull": true,
"autoincrement": false
},
"expires_at": {
"name": "expires_at",
"type": "integer",
"primaryKey": false,
"notNull": true,
"autoincrement": false
},
"created_at": {
"name": "created_at",
"type": "integer",
"primaryKey": false,
"notNull": false,
"autoincrement": false
},
"updated_at": {
"name": "updated_at",
"type": "integer",
"primaryKey": false,
"notNull": false,
"autoincrement": false
}
},
"indexes": {},
"foreignKeys": {},
"compositePrimaryKeys": {},
"uniqueConstraints": {},
"checkConstraints": {}
}
},
"views": {},
"enums": {},
"_meta": {
"schemas": {},
"tables": {},
"columns": {}
},
"internal": {
"indexes": {}
}
}

@ -0,0 +1,13 @@
{
"version": "7",
"dialect": "sqlite",
"entries": [
{
"idx": 0,
"version": "6",
"when": 1749233567397,
"tag": "0000_violet_agent_zero",
"breakpoints": true
}
]
}

9761
package-lock.json generated

File diff suppressed because it is too large Load Diff

@ -5,50 +5,56 @@
"scripts": { "scripts": {
"dev": "run-s astro:sync astro:dev", "dev": "run-s astro:sync astro:dev",
"build": "run-s astro:build", "build": "run-s astro:build",
"astro:server": "bun run ./dist/server/entry.mjs",
"drizzle:migrate": "drizzle-kit migrate",
"drizzle:generate": "drizzle-kit generate",
"start": "run-s drizzle:migrate astro:server",
"astro:sync": "astro sync", "astro:sync": "astro sync",
"astro:dev": "astro dev", "astro:dev": "astro dev",
"astro:build": "astro check && astro build" "astro:build": "astro check && astro build"
}, },
"dependencies": { "dependencies": {
"@astrojs/check": "^0.9.4", "@astrojs/check": "^0.9.4",
"@astrojs/node": "^9.4.3", "@astrojs/node": "9.0.0",
"@astrojs/preact": "^4.1.1", "@astrojs/preact": "4.0.0",
"@fontsource-variable/material-symbols-outlined": "^5.2.21", "@fontsource-variable/material-symbols-outlined": "^5.1.1",
"@fontsource/iosevka": "^5.2.5", "@fontsource/iosevka": "^5.0.11",
"@fontsource/mononoki": "^5.2.5", "@fontsource/mononoki": "^5.0.11",
"@fontsource/open-sans": "^5.2.6", "@fontsource/open-sans": "^5.0.24",
"@fontsource/source-code-pro": "^5.2.6", "@fontsource/source-code-pro": "^5.0.16",
"@fontsource/source-sans-pro": "^5.2.5", "@fontsource/source-sans-pro": "^5.0.8",
"@fontsource/space-mono": "^5.2.8", "@fontsource/space-mono": "^5.0.20",
"@libsql/client": "^0.15.8",
"@phosphor-icons/core": "^2.1.1", "@phosphor-icons/core": "^2.1.1",
"@phosphor-icons/react": "^2.1.10", "@phosphor-icons/react": "^2.1.7",
"@preact/signals": "^1.3.2", "@preact/signals": "^1.3.0",
"@types/jsdom": "^21.1.7", "@types/jsdom": "^21.1.7",
"astro": "^5.13.7", "astro": "5.1.0",
"fuse.js": "^7.1.0", "better-auth": "^1.2.8",
"katex": "^0.16.22", "better-sqlite3": "^11.10.0",
"dotenv": "^16.5.0",
"drizzle-orm": "^0.44.2",
"drizzle-kit": "^0.31.1",
"fuse.js": "^7.0.0",
"katex": "^0.16.9",
"lucide-static": "^0.468.0", "lucide-static": "^0.468.0",
"marked": "^15.0.12", "marked": "^15.0.6",
"node-addon-api": "^8.5.0", "preact": "^10.19.6",
"node-gyp": "^11.4.2", "npm-run-all": "^4.1.5",
"preact": "^10.27.2", "typescript": "^5.3.3"
"sharp": "^0.34.3",
"typescript": "^5.9.2"
}, },
"devDependencies": { "devDependencies": {
"@astrojs/mdx": "^4.3.5", "@astrojs/mdx": "4.0.2",
"@rollup/plugin-yaml": "^4.1.2", "@rollup/plugin-yaml": "^4.1.2",
"@types/katex": "^0.16.7", "@types/katex": "^0.16.7",
"jsdom": "^24.1.3", "jsdom": "^24.1.1",
"linkedom": "^0.18.12", "linkedom": "^0.18.4",
"npm-run-all": "^4.1.5", "prettier": "^3.5.0",
"prettier": "^3.6.2",
"prettier-plugin-astro": "^0.14.1", "prettier-plugin-astro": "^0.14.1",
"rehype-autolink-headings": "^7.1.0", "rehype-autolink-headings": "^7.1.0",
"rehype-slug": "^6.0.0", "rehype-slug": "^6.0.0",
"remark-math": "^6.0.0", "remark-math": "^6.0.0",
"remark-toc": "^9.0.0", "remark-toc": "^9.0.0",
"sass": "^1.92.1", "tsx": "^4.7.1"
"tsx": "^4.20.5"
} }
} }

Binary file not shown.

Before

Width:  |  Height:  |  Size: 20 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 21 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 25 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 26 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 877 KiB

@ -0,0 +1,28 @@
import { betterAuth } from 'better-auth'
import { genericOAuth } from 'better-auth/plugins'
import { drizzleAdapter } from 'better-auth/adapters/drizzle'
import * as authSchema from '@/db/auth-schema'
import db from '@/db'
export const auth = betterAuth({
database: drizzleAdapter(db, {
provider: 'sqlite',
schema: authSchema,
}),
plugins: [
genericOAuth({
config: [
{
providerId: 'unipi',
clientId: process.env.OAUTH_CLIENT_ID!,
clientSecret: process.env.OAUTH_CLIENT_SECRET!,
scopes: process.env.OAUTH_SCOPES!.split(' '),
userInfoUrl: process.env.OAUTH_USER_INFO_URL!,
authorizationUrl: process.env.OAUTH_AUTH_URL!,
tokenUrl: `${process.env.OAUTH_TOKEN_HOST}${process.env.OAUTH_TOKEN_PATH}`,
redirectURI: process.env.OAUTH_REDIRECT_URL!,
},
],
}),
],
})

@ -41,13 +41,7 @@ export const ComboBox = ({
<PhosphorIcon name="caret-down" /> <PhosphorIcon name="caret-down" />
</div> </div>
{open && ( {open && (
<div <div class={clsx('dropdown', cloak && 'invisible')} ref={el => el && setItemWidth(el.offsetWidth)}>
class={clsx('dropdown', cloak && 'invisible')}
ref={el => {
if (!el) return
setItemWidth(el.offsetWidth)
}}
>
{Object.keys(children).map(key => ( {Object.keys(children).map(key => (
<div <div
class="option" class="option"

@ -1,5 +1,5 @@
import { useEffect, useState } from 'preact/hooks' import { useEffect, useState } from 'preact/hooks'
import { FunnelIcon } from '@phosphor-icons/react' import { Funnel } from '@phosphor-icons/react'
import { marked } from 'marked' import { marked } from 'marked'
import extendedLatex from '@/client/lib/marked-latex' import extendedLatex from '@/client/lib/marked-latex'
@ -70,7 +70,7 @@ export const DomandeEsamiCourse = ({ course }: Props) => {
{courseTags.length > 1 && ( {courseTags.length > 1 && (
<div class="card filter"> <div class="card filter">
<div class="grid-h"> <div class="grid-h">
<FunnelIcon /> <Funnel />
<strong>Filtra Tag</strong> <strong>Filtra Tag</strong>
</div> </div>
<div class="flex-row-wrap"> <div class="flex-row-wrap">

@ -1,4 +1,5 @@
import { useState, useEffect } from 'preact/hooks' import { useState, useEffect } from 'preact/hooks'
import { render } from 'preact'
// Tipi per la gestione dei dati // Tipi per la gestione dei dati
type TipoStudente = 'triennale' | 'magistrale' type TipoStudente = 'triennale' | 'magistrale'

@ -0,0 +1,5 @@
import { createAuthClient } from 'better-auth/client'
import { genericOAuthClient } from 'better-auth/client/plugins'
export const authClient = createAuthClient({
plugins: [genericOAuthClient()],
})

@ -9,7 +9,7 @@ const extBlock = options => ({
start(src) { start(src) {
return src.match(/\$\$[^\$]/)?.index ?? -1 return src.match(/\$\$[^\$]/)?.index ?? -1
}, },
tokenizer(src, _tokens) { tokenizer(src, tokens) {
const match = /^\$\$([^\$]+)\$\$/.exec(src) const match = /^\$\$([^\$]+)\$\$/.exec(src)
return match ? { type: 'latex-block', raw: match[0], formula: match[1] } : undefined return match ? { type: 'latex-block', raw: match[0], formula: match[1] } : undefined
}, },
@ -25,7 +25,7 @@ const extInline = options => ({
start(src) { start(src) {
return src.match(/\$[^\$]/)?.index ?? -1 return src.match(/\$[^\$]/)?.index ?? -1
}, },
tokenizer(src, _tokens) { tokenizer(src, tokens) {
const match = /^\$([^\$]+)\$/.exec(src) const match = /^\$([^\$]+)\$/.exec(src)
return match ? { type: 'latex', raw: match[0], formula: match[1] } : undefined return match ? { type: 'latex', raw: match[0], formula: match[1] } : undefined
}, },

@ -32,17 +32,17 @@ const guidesCollection = defineCollection({
}) })
// Per ora sono su un sito a parte ma prima o poi verranno migrati qui // Per ora sono su un sito a parte ma prima o poi verranno migrati qui
// const seminariettiCollection = defineCollection({ const seminariettiCollection = defineCollection({
// type: 'content', type: 'content',
// schema: z.object({ schema: z.object({
// title: z.string(), title: z.string(),
// description: z.string(), description: z.string(),
// author: z.string(), author: z.string(),
// publishDate: z.date(), publishDate: z.date(),
// eventDate: z.date(), eventDate: z.date(),
// tags: z.array(z.string()), tags: z.array(z.string()),
// }), }),
// }) })
const metaCollection = defineCollection({ const metaCollection = defineCollection({
type: 'content', type: 'content',
@ -53,6 +53,6 @@ const metaCollection = defineCollection({
export const collections = { export const collections = {
news: newsCollection, news: newsCollection,
guides: guidesCollection, guides: guidesCollection,
// seminarietti: seminariettiCollection, seminarietti: seminariettiCollection,
meta: metaCollection, meta: metaCollection,
} }

@ -1,80 +0,0 @@
---
id: stampare-via-ssh
title: Stampare via SSH
description: Istruzioni per stampare in dipartimento da remoto, tramite SSH 🖨
author: Antonio De Lucreziis, Francesco Minnocci
tags: [linux, ssh, stampanti]
---
Per stampare in dipartimento non bisogna per forza usare i computer dei laboratori, possiamo che stampare direttamente da remoto tramite SSH. Vediamo come fare!
Se non l'avete mai fatto per prima cosa bisogna poter accedere da remoto ad una macchina chiamata "login", il cui indirizzo è `login.dm.unipi.it`. Per fare l'accesso possiamo usare il seguente comando con l'account di Ateneo (non quello Poisson!)
```bash shell
ssh USERNAME_ATENEO@login.dm.unipi.it
```
Una volta connessi possiamo stampare utilizzando il comando `lpr` seguito dal nome del file che vogliamo stampare. Prima però serve trasferire il file che vogliamo stampare sulla macchina "login". Per fare ciò possiamo usare il comando `scp`: per prima cosa usciamo dalla macchina "login" (premere `Ctrl+D` oppure scrivendo `exit`), andiamo nella cartella dove si trova il file che vogliamo stampare e poi eseguiamo il comando:
```bash shell
scp NOME_FILE.pdf USERNAME_ATENEO@login.dm.unipi.it:~/Documents
```
Dove `NOME_FILE.pdf` è il nome del file che vogliamo stampare e `Documents` è un esempio di cartella dove vogliamo trasferirlo. Una volta trasferito il file possiamo rifare ssh su "login" e stampare il file con il comando:
```bash shell
lpr Documents/NOME_FILE.pdf
```
Alternativamente possiamo stampare direttamente il file senza trasferirlo con il comando:
```bash shell
cat NOME_FILE.pdf | ssh USERNAME_ATENEO@login.dm.unipi.it lpr OPZIONI... -
```
Qui, `[OPZIONI...]` sono le opzioni che possiamo passare a `lpr` (vedi sotto). L'ultimo trattino "`-`" è molto importante e indica che il file da stampare è quello in standard input. Più precisamente, `cat NOME_FILE.pdf` invia il contenuto del file `NOME_FILE.pdf` allo standard output e `|` lo ridireziona a input di `ssh`, che a sua volta lo passa a `lpr` via rete.
## Opzioni di `lpr`
Il comando `lpr` accetta alcune opzioni che possono essere utili:
- `-P` seguito dal nome della stampante: permette di specificare la stampante su cui stampare, le stampanti disponibili in dipartimento sono
- `cdc4` che è la stampante di default e si trova in Aula 4
- `cdclf` che si trova al piano terra nel corridoio dopo l'Aula 4
- `cdc3` che si trova in Aula 3 (è un po' vecchia ma di solito funziona)
- `-#` seguito dal numero di copie: permette di specificare il numero di copie da stampare. In realtà questa opzione non funziona per vari motivi arcani e se uno passa `-#N` per stampare $N$ copie, la stampante stampa $N^2$ copie. (Questo ha scaturito una serie di ragionamenti sul modo ottimo di decomporre $N$ come somma di quadrati [con tanto di sito di comodo](https://shortest-sum-of-squares.netlify.app/)...)
- `-o sides=two-sided-long-edge`: permette di stampare **fronte-retro** (che dovrebbe essere già il default)
- `-o sides=two-sided-short-edge`: permette di stampare fronte-retro con "la rilegatura" delle pagine sul lato corto
- `-o sides=one-sided`: permette di stampare _solo fronte_, comodo per stampare i meme di laurea
- `-o fit-to-page`: permette di ridimensionare il documento per farlo entrare in un foglio (è buona prassi passare sempre questa opzione)
- `-o media=a4`: permette di specificare il formato del foglio, di default è A4 quindi non dovrebbere servire
## Altre comodità
Stampare da remoto porta anche altre comodità, ad esempio possiamo interrompere un file che abbiamo mandato in stampa per sbaglio con il comando (sempre tutti comandi da eseguire su "login")
```bash shell
cancel -a
```
> Attenzione, il comando sopra cancella tutta la propria coda di stampa, non solo l'ultimo lavoro inviato.
Alternativamente possiamo vedere lo stato della coda di stampa con il comando
```bash shell
lpq -a
```
e cancellare un lavoro con uno specifico ID con
```bash shell
cancel ID
```

@ -1,6 +1,6 @@
--- ---
title: Calcola la tua media ed il voto di laurea con il nuovissimo calcolatore del PHC! title: Calcola la tua media ed il voto di laurea con il nuovissimo calcolatore del PHC!
description: È ora disponibile uno strumento per calcolare la propria media pesata e il voto di ammissione alla laurea secondo le regole del dipartimento. description: È ora disponibile uno strumento per calcolare la propria media pesata e il voto di ammissione alla laurea secondo le regole del dipartimento
publishDate: 2025-06-26 publishDate: 2025-06-26
--- ---

@ -1,46 +0,0 @@
---
title: Esplora i meme dell'aula studenti online!
description: |
Gli storici meme sono stati staccati per i lavori, ma non disperare: li potrai vedere su una nuova pagina.
publishDate: 2025-06-26
---
# Esplora i meme dell'aula studenti online!
Visti gli imminenti lavori che occuperanno l'aula studenti, ad inizio Settembre tutti i meme sulle pareti sono stati staccati e riposti temporaneamente in PHC; qui sotto trovate alcuni timelapse della giornata:
<div class="grid-h-split">
<video controls>
<source src="https://static.phc.dm.unipi.it/timelapse-nord.mp4" type="video/mp4">
Your browser does not support the video tag.
</video>
<video controls>
<source src="https://static.phc.dm.unipi.it/timelapse-sud.mp4" type="video/mp4">
Your browser does not support the video tag.
</video>
<video controls>
<source src="https://static.phc.dm.unipi.it/timelapse-termosifone.mp4" type="video/mp4">
Your browser does not support the video tag.
</video>
</div>
Per questo motivo, li abbiamo scansionati ed abbiamo creato una bacheca per poter contemplare i meme dovunque voi siate 🧳
Eccovi dunque il link alla pagina, buon divertimento:
![Screenshot Bacheca](/images/misc/screenshot-bacheca.png)
<p align="center">
<a href="https://meme.phc.dm.unipi.it">meme.phc.dm.unipi.it</a>
</p>
## Coming Soon
Prima o poi faremo anche una mappa interattiva della stanza, basata sul seguente modello 3D ricostruito con tecniche di fotogrammetria:
<video controls>
<source src="https://static.phc.dm.unipi.it/3d-scan-preview.mp4" type="video/mp4">
Your browser does not support the video tag.
</video>
> Disclaimer: Se volessi rimuovere una tua immagine da questa pagina, scrivici pure a <a href="mailto:macchinisti@lists.dm.unipi.it">macchinisti@lists.dm.unipi.it</a> e ce ne occuperemo.

@ -1,34 +0,0 @@
---
title: Non avete attivato la 2FA entro il primo dicembre? Ecco come recuperare l'accesso
description: |
Se non avete attivato l'autenticazione a due fattori entro il primo dicembre, siete bloccati fuori dall'account Unipi. La procedura di recupero passa dal supporto tecnico.
publishDate: 2025-12-05
---
# Non avete attivato la 2FA entro il primo dicembre? Ecco come recuperare l'accesso
Se siamo rimasti fuori dal nostro account Unipi perché non abbiamo attivato l'autenticazione a due fattori entro il primo dicembre, il percorso di recupero è piuttosto lineare.
Il primo passo è mandare una mail a
> <a href="mailto:help.polo2@ticket.unipi.it">help.polo2@ticket.unipi.it</a>
usando un indirizzo email personale (quello di ateneo è bloccato). Nel messaggio dobbiamo specificare chiaramente il problema e includere il nostro indirizzo email d'ateneo.
Dopo aver inviato la richiesta, tocca aspettare. Il supporto tecnico resetta manualmente lo status 2FA e ci manda una conferma. A quel punto possiamo procedere con l'attivazione seguendo le istruzioni ufficiali:
> https://it.unipi.it/configurazioni/mfa/autenticazione-a-piu-fattori-mfa-microsoft-365/
**Nota tecnica:** L'app Microsoft Authenticator non è obbligatoria! Qualsiasi autenticatore compatibile con [TOTP](https://en.wikipedia.org/wiki/Time-based_one-time_password) va bene: Google Authenticator, Bitwarden, Authy e altre alternative funzionano perfettamente. L'unica differenza è che l'app Microsoft Authenticator permette di ricevere notifiche l'accesso che consente di fare login con meno click.
Per utilizzare una di queste altre app (invece di Microsoft Authenticator), seguire questi step nella creazione del metodo di autenticazione:
1. Selezionare **"Microsoft Authenticator"**
![Selezionare Microsoft Authenticator](/images/misc/microsoft-2fa-other-app-1.webp)
2. Selezionare **"Configura un'app di autenticazione diversa"**
![Selezionare app diversa](/images/misc/microsoft-2fa-other-app-2.webp)
3. A questo punto il procedimento cambia in base all'app. In generale verrà richiesto di scannerizzare un QR code e di verificare il corretto funzionamento inserendo la TOTP (codice a 6 cifre)

@ -1117,400 +1117,6 @@ questions:
tags: tags:
- 2023 - 2023
# Raccolta di Istituzioni di Geometria da Valeria
# Raccolta di Istituzioni di Geometria da Fra
- course: istituzioni-di-geometria
content: |
Intorno tubolare, esistenza
tags:
- 2025
- course: istituzioni-di-geometria
content: |
Gruppi di Lie, esiste un unico sottogruppo connesso con data sottoalgebra
tags:
- 2025
- course: istituzioni-di-geometria
content: |
Frobenius
tags:
- 2025
- course: istituzioni-di-geometria
content: |
Invarianza omotopica di de Rham
tags:
- 2025
- course: istituzioni-di-geometria
content: |
Mayer-Vietoris
tags:
- 2025
- course: istituzioni-di-geometria
content: |
Cartan-Hadamard
tags:
- 2025
- course: istituzioni-di-geometria
content: |
Lemma di Gauss
tags:
- 2025
- course: istituzioni-di-geometria
content: |
Whitney
tags:
- 2025
- course: istituzioni-di-geometria
content: |
Localmente euclideo se e solo se R=0
tags:
- 2025
- course: istituzioni-di-geometria
content: |
Mayer-Vietoris a supporto compatto
tags:
- 2025
- course: istituzioni-di-geometria
content: |
Orientabilità di RP^n e CP^n
tags:
- 2025
- course: istituzioni-di-geometria
content: |
Lemma di Poincaré (dimostrare anche che mappe omotope inducono stesso pullback sulla coomologia), brevissimo accenno al caso a supporto compatto (nessuna dimostrazione)
tags:
- 2025
- course: istituzioni-di-geometria
content: |
Cos'è il trasporto parallelo (tutti i dettagli di buona definizione a partire dalla definizione di campo parallelo)? Se metto una metrica cosa succede? (Statement delle condizioni equivalenti di compatibilità senza dimostrazione)
tags:
- 2025
- course: istituzioni-di-geometria
content: |
Dimostra che l'algebra di Lie associata ad un gruppo di Lie è un'algebra di Lie in senso astratto (voleva giusto sentirsi dire che il bracket di campi invarianti a sinistra è invariante a sinistra senza dimostrazione)
tags:
- 2025
- course: istituzioni-di-geometria
content: |
Teorema di corrispondenza algebre di lie
tags:
- 2025
- course: istituzioni-di-geometria
content: |
Quando gli spazi proiettivi reali sono orientabili
tags:
- 2025
- course: istituzioni-di-geometria
content: |
Esistenza dell'intorno tubolare (senza dimostrazione degli "esercizi")
tags:
- 2025
- course: istituzioni-di-geometria
content: |
Dualità di Poincaré 2
tags:
- 2025
- course: istituzioni-di-geometria
content: |
Definizione di connessione metrica e condizioni equivalenti (solo enunciato)
tags:
- 2025
- course: istituzioni-di-geometria
content: |
Hopf-Rinow
tags:
- 2025
- course: istituzioni-di-geometria
content: |
Isotopia Ambiente
tags:
- 2025
- course: istituzioni-di-geometria
content: |
Differenziale Esterno
tags:
- 2025
- course: istituzioni-di-geometria
content: |
Stokes
tags:
- 2025
- course: istituzioni-di-geometria
content: |
Raddrizzamento simultaneo
tags:
- 2025
- course: istituzioni-di-geometria
content: |
Definizione di fibrato tangente
tags:
- 2025
- course: istituzioni-di-geometria
content: |
Definizione di intorno tubolare e esistenza
tags:
- 2025
- course: istituzioni-di-geometria
content: |
Teorema di Frobenius
tags:
- 2025
- course: istituzioni-di-geometria
content: |
Piatto se e solo se localmente isometrico
tags:
- 2025
- course: istituzioni-di-geometria
content: |
Quando lo spazio proiettivo reale è orientabile? E quello complesso?
tags:
- 2025
- course: istituzioni-di-geometria
content: |
Isotopia e isotopia ambiente
tags:
- 2025
- course: istituzioni-di-geometria
content: |
Differenziale k-forme
tags:
- 2025
- course: istituzioni-di-geometria
content: |
Corrispondenza tra sottogruppi di lie connessi e sottoalgebre di lie
tags:
- 2025
- course: istituzioni-di-geometria
content: |
Torsione di una connessione
tags:
- 2025
- course: istituzioni-di-geometria
content: |
Dualità di poincarè (perché la mappa DP passa in coomologia?)
tags:
- 2025
- course: istituzioni-di-geometria
content: |
Parlare dell'algebra di Lie associata a un Gruppo di Lie
tags:
- 2025
- course: istituzioni-di-geometria
content: |
Enunciato di Frobenius, e perché una distribuzione è integrabile sse è loc. costante
tags:
- 2025
- course: istituzioni-di-geometria
content: |
Poincaré Dualità
tags:
- 2025
- course: istituzioni-di-geometria
content: |
Conseguenze (Betti numeri, specchiabilità)
tags:
- 2025
- course: istituzioni-di-geometria
content: |
Distribuzioni, esempi di distribuzioni che (non) sono embeddings
tags:
- 2025
- course: istituzioni-di-geometria
content: |
Trasporto parallelo e geodetiche
tags:
- 2025
- course: istituzioni-di-geometria
content: |
Esempio di varietà non geodeticamente completa, si può avere compatto? (Hopf rinow)
tags:
- 2025
- course: istituzioni-di-geometria
content: |
Esistenza di forma volume per varietà orientate e di struttura riemanniana sui fibrati
tags:
- 2025
- course: istituzioni-di-geometria
content: |
Definizione di foliazione
tags:
- 2025
- course: istituzioni-di-geometria
content: |
Localmente euclidea sse piatto
tags:
- 2025
- course: istituzioni-di-geometria
content: |
Teorema di Stokes
tags:
- 2025
- course: istituzioni-di-geometria
content: |
Teorema di Hopf-Rinow
tags:
- 2025
- course: istituzioni-di-geometria
content: |
Orientabilità di CP^n
tags:
- 2025
- course: istituzioni-di-geometria
content: |
Definizione di forma volume ed esistenza
tags:
- 2025
- course: istituzioni-di-geometria
content: |
Stokes (dim.)
tags:
- 2025
- course: istituzioni-di-geometria
content: |
isotopia e isotopia ambiente (def. + teo.)
tags:
- 2025
- course: istituzioni-di-geometria
content: |
tensore Riemann (def.)
tags:
- 2025
- course: istituzioni-di-geometria
content: |
teo. sottoalgebre di Lie
tags:
- 2025
- course: istituzioni-di-geometria
content: |
esistenza metrica Lorentziana e Riemanniana
tags:
- 2025
- course: istituzioni-di-geometria
content: |
derivata di k-forme
tags:
- 2025
- course: istituzioni-di-geometria
content: |
lemma Poincaré e teo. sull'omotopia
tags:
- 2025
- course: istituzioni-di-geometria
content: |
Cos'è l'algebra di Lie (molto veloce)
tags:
- 2025
- course: istituzioni-di-geometria
content: |
Cos'è la connessione di Levi Civita ed elencare le forme equivalenti di "compatibilità con la metrica"
tags:
- 2025
- course: istituzioni-di-geometria
content: |
Definire la torsione e fare i conti per mostrare che è un tensore
tags:
- 2025
- course: istituzioni-di-geometria
content: |
Teorema di Whitney (caso compatto e caso generale).
tags:
- 2025
- course: istituzioni-di-geometria
content: |
M ammette struttura lorentziana orientabile temporalmente sse pettinabile.
tags:
- 2025
- course: istituzioni-di-geometria
content: |
Teorema su sottoalgebre di Lie
tags:
- 2025
- course: istituzioni-di-geometria
content: |
geodetiche (praticamente solo quale è l'equazione e perché ha senso fare la nabla di un campo su una curva)
tags:
- 2025
- course: istituzioni-di-geometria
content: |
Frobenius, enunciato e dimostrazione (dando per scontate le def.equivalenti di foliazione)
tags:
- 2025
- course: istituzioni-di-geometria
content: |
Whitney (entrambi)
tags:
- 2025
- course: istituzioni-di-geometria
content: |
quando il proiettivo è orientabile (ed enunciato del teorema da cui segue)
tags:
- 2025
# Raccolta di Istituzioni di Geometria da Fra # Raccolta di Istituzioni di Geometria da Fra
- course: istituzioni-di-geometria - course: istituzioni-di-geometria

@ -0,0 +1,57 @@
import { sqliteTable, text, integer } from 'drizzle-orm/sqlite-core'
export const user = sqliteTable('user', {
id: text('id').primaryKey(),
name: text('name').notNull(),
email: text('email').notNull().unique(),
emailVerified: integer('email_verified', { mode: 'boolean' })
.$defaultFn(() => false)
.notNull(),
image: text('image'),
createdAt: integer('created_at', { mode: 'timestamp' })
.$defaultFn(() => /* @__PURE__ */ new Date())
.notNull(),
updatedAt: integer('updated_at', { mode: 'timestamp' })
.$defaultFn(() => /* @__PURE__ */ new Date())
.notNull(),
})
export const session = sqliteTable('session', {
id: text('id').primaryKey(),
expiresAt: integer('expires_at', { mode: 'timestamp' }).notNull(),
token: text('token').notNull().unique(),
createdAt: integer('created_at', { mode: 'timestamp' }).notNull(),
updatedAt: integer('updated_at', { mode: 'timestamp' }).notNull(),
ipAddress: text('ip_address'),
userAgent: text('user_agent'),
userId: text('user_id')
.notNull()
.references(() => user.id, { onDelete: 'cascade' }),
})
export const account = sqliteTable('account', {
id: text('id').primaryKey(),
accountId: text('account_id').notNull(),
providerId: text('provider_id').notNull(),
userId: text('user_id')
.notNull()
.references(() => user.id, { onDelete: 'cascade' }),
accessToken: text('access_token'),
refreshToken: text('refresh_token'),
idToken: text('id_token'),
accessTokenExpiresAt: integer('access_token_expires_at', { mode: 'timestamp' }),
refreshTokenExpiresAt: integer('refresh_token_expires_at', { mode: 'timestamp' }),
scope: text('scope'),
password: text('password'),
createdAt: integer('created_at', { mode: 'timestamp' }).notNull(),
updatedAt: integer('updated_at', { mode: 'timestamp' }).notNull(),
})
export const verification = sqliteTable('verification', {
id: text('id').primaryKey(),
identifier: text('identifier').notNull(),
value: text('value').notNull(),
expiresAt: integer('expires_at', { mode: 'timestamp' }).notNull(),
createdAt: integer('created_at', { mode: 'timestamp' }).$defaultFn(() => /* @__PURE__ */ new Date()),
updatedAt: integer('updated_at', { mode: 'timestamp' }).$defaultFn(() => /* @__PURE__ */ new Date()),
})

@ -0,0 +1,4 @@
import 'dotenv/config'
import { drizzle } from 'drizzle-orm/libsql'
export default drizzle(process.env.DB_FILE_NAME!)

@ -0,0 +1,33 @@
import { sqliteTable, text } from 'drizzle-orm/sqlite-core'
export * from './auth-schema'
import { user } from './auth-schema'
export const usersTable = sqliteTable('users', {
id: text('id').references(() => user.id),
username: text('username').notNull(),
fullName: text('full_name'),
email: text('email'),
})
export const accountsTable = sqliteTable('accounts', {
userId: text('user_id').references(() => user.id),
provider: text('provider').notNull(),
username: text('username').notNull(),
})
export const poissonRequestsTable = sqliteTable('poisson_requests', {
userId: text('user_id').references(() => usersTable.id),
requestType: text('request_type').notNull(), // "link" or "create"
comments: text('comments'), // contains username suggestions
})
export const appuntiTable = sqliteTable('appunti', {
userId: text('user_id').references(() => usersTable.id),
hash: text('hash').notNull(),
link: text('link'),
visibility: text('visibility').notNull(), // "public", "internal", "private"
title: text('title').notNull(),
description: text('description'),
tags: text('tags').notNull(), // Array of strings, e.g. ["#geometria-2", "#511aa", "#2017-18", "#frigerio"]
})

8
src/env.d.ts vendored

@ -1,2 +1,10 @@
/// <reference path="../.astro/types.d.ts" /> /// <reference path="../.astro/types.d.ts" />
/// <reference types="astro/client" /> /// <reference types="astro/client" />
declare namespace App {
// Note: 'import {} from ""' syntax does not work in .d.ts files.
interface Locals {
user: import('better-auth').User | null
session: import('better-auth').Session | null
}
}

@ -0,0 +1,18 @@
import { auth } from '@/auth'
import { defineMiddleware } from 'astro:middleware'
export const onRequest = defineMiddleware(async (context, next) => {
const isAuthed = await auth.api.getSession({
headers: context.request.headers,
})
if (isAuthed) {
context.locals.user = isAuthed.user
context.locals.session = isAuthed.session
} else {
context.locals.user = null
context.locals.session = null
}
return next()
})

@ -0,0 +1,8 @@
import { auth } from '@/auth'
import type { APIRoute } from 'astro'
export const ALL: APIRoute = async ctx => {
// If you want to use rate limiting, make sure to set the 'x-forwarded-for' header to the request headers from the context
// ctx.request.headers.set("x-forwarded-for", ctx.clientAddress);
return auth.handler(ctx.request)
}

@ -0,0 +1,8 @@
<script>
import { authClient } from '@/client/auth-client' //import the auth client
await authClient.signIn.oauth2({
providerId: 'unipi',
callbackURL: '/profilo', // the path to redirect to after the user is authenticated
})
</script>

@ -11,11 +11,7 @@ import Card from '@/components/Card.astro'
const news = await getCollection('news') const news = await getCollection('news')
// const galleryCollage = await Astro.glob('@/assets/gallery/*.jpg') const galleryCollage = await Astro.glob('@/assets/gallery/*.jpg')
const galleryCollage: { default: ImageMetadata }[] = Object.values(
import.meta.glob('@/assets/gallery/*.jpg', { eager: true }),
)
--- ---
<PageLayout title="PHC" pageTags="homepage"> <PageLayout title="PHC" pageTags="homepage">
@ -51,9 +47,7 @@ const galleryCollage: { default: ImageMetadata }[] = Object.values(
<div class="card-list"> <div class="card-list">
{ {
news news.toReversed().map(newsItem => (
.sort((s, t) => -s.id.localeCompare(t.id))
.map(newsItem => (
<Card> <Card>
<a href={`/notizie/${newsItem.slug}`} class="title"> <a href={`/notizie/${newsItem.slug}`} class="title">
{newsItem.data.title} {newsItem.data.title}
@ -204,16 +198,14 @@ const galleryCollage: { default: ImageMetadata }[] = Object.values(
{ {
galleryCollage.map((module, i) => { galleryCollage.map((module, i) => {
const src = module.default.src const src = module.default.src
const filename = src.split('/').at(-1)?.split('?').at(0)?.split('.').at(0) const filename = src.split('/').at(-1).split('?').at(0).split('.').at(0)
if (!filename) return null
const [rows, cols] = filename.includes('@') const [rows, cols] = filename.includes('@')
? (filename ? filename
.split('@') .split('@')
.at(-1) .at(-1)
?.split('x') .split('x')
.map((s: string) => parseInt(s)) ?? [1, 1]) .map((s: string) => parseInt(s))
: [1, 1] : [1, 1]
return ( return (

@ -1,5 +1,6 @@
--- ---
import PageLayout from '../layouts/PageLayout.astro' import PageLayout from '../layouts/PageLayout.astro'
import '@/styles/pages/login.css'
--- ---
<PageLayout title="Login | PHC" pageTags="login"> <PageLayout title="Login | PHC" pageTags="login">
@ -7,16 +8,25 @@ import PageLayout from '../layouts/PageLayout.astro'
<!-- form with username and password, and a button for oauth login --> <!-- form with username and password, and a button for oauth login -->
<form action="/login" method="post"> <form action="/login" method="post">
<h3 class="center">Accedi con Poisson</h3> <h3 class="center"><strike>Accedi con Poisson</strike> (Work in Progress)</h3>
<input type="text" id="username" placeholder="Username" name="username" required />
<input type="password" id="password" placeholder="Password" name="password" required />
<button class="primary center" type="submit">Login</button> <div class="row">
<span class="material-symbols-outlined">person</span>
<input type="text" id="username" placeholder="Username" name="username" required disabled />
</div>
<div class="row">
<span class="material-symbols-outlined">key</span>
<input type="password" id="password" placeholder="Password" name="password" required disabled />
</div>
<button class="primary center" type="submit" disabled>Login</button>
<hr /> <hr />
<h3 class="center">Accedi con Ateneo</h3> <h3 class="center">Accedi con Ateneo</h3>
<a href="/auth/ateneo" class="primary center" role="button">Login</a> <a href="/auth/ateneo" class="primary center" role="button">
<span class="material-symbols-outlined">account_balance</span>
Login
</a>
</form> </form>
<!-- <span class="material-symbols-outlined">person</span> -->
</PageLayout> </PageLayout>

@ -12,9 +12,7 @@ const news = await getCollection('news')
<h1><a href="/notizie">Notizie</a></h1> <h1><a href="/notizie">Notizie</a></h1>
<div class="card-list"> <div class="card-list">
{ {
news news.toReversed().map(newsItem => (
.sort((s, t) => -s.id.localeCompare(t.id))
.map(newsItem => (
<div class="card"> <div class="card">
<a href={`/notizie/${newsItem.slug}`} class="title"> <a href={`/notizie/${newsItem.slug}`} class="title">
{newsItem.data.title} {newsItem.data.title}

@ -0,0 +1,40 @@
---
import PageLayout from '../layouts/PageLayout.astro'
import '@/styles/pages/login.css'
if (!Astro.locals.user) {
return Astro.redirect('/login')
}
const { name, email } = Astro.locals.user
---
<PageLayout title="Profilo | PHC">
<div class="card large">
<div class="title">Profilo di {name}</div>
<div class="text">
<p>
<strong>Nome:</strong>
{name}
<br />
<strong>Email:</strong>
{email}
<br />
</p>
<p>
Benvenuto nel tuo profilo! Qui puoi visualizzare le tue informazioni personali e modificare le tue
impostazioni.
</p>
</div>
</div>
<div class="card">
<div class="title">Impostazioni</div>
<div class="text">
<p>
Al momento non sono disponibili impostazioni modificabili. Se hai bisogno di assistenza, contatta il
supporto tecnico.
</p>
</div>
</div>
</PageLayout>

@ -1,27 +0,0 @@
/* This file is here for historical reasons but is not used anymore */
@layer page {
/*
.login {
background: #ddfaff;
main {
justify-self: center;
display: flex;
flex-direction: column;
align-items: center;
max-width: 80ch;
padding: 3rem 0;
gap: 3rem;
h3 {
font-size: 28px;
font-weight: 600;
}
}
}
*/
}

@ -1042,20 +1042,6 @@ $news-accent-bg: #f8e8b1;
grid-auto-flow: row; grid-auto-flow: row;
} }
.grid-h-split {
display: grid;
place-content: center;
place-items: center;
gap: 1rem;
grid-auto-flow: column;
grid-auto-columns: 1fr;
@media screen and (max-width: 1024px) {
grid-auto-flow: row;
grid-auto-columns: auto;
}
}
.clickable { .clickable {
cursor: pointer; cursor: pointer;
} }

@ -21,6 +21,8 @@ Controls - for things like buttons, input, select
display: grid; display: grid;
place-content: center; place-content: center;
grid-auto-flow: column;
gap: 0.25rem;
&:hover { &:hover {
transform: translate(-1px, -1px); transform: translate(-1px, -1px);
@ -206,6 +208,23 @@ Controls - for things like buttons, input, select
background: #4ea2b1; background: #4ea2b1;
} }
} }
&:disabled {
background: #aaa;
color: #666;
cursor: not-allowed;
&:active {
transform: none;
box-shadow: 4px 4px 0 0 #222;
}
&:hover {
background: #aaa;
transform: none;
box-shadow: 4px 4px 0 0 #222;
}
}
} }
hr { hr {
@ -217,6 +236,25 @@ Controls - for things like buttons, input, select
margin-top: 1rem; margin-top: 1rem;
} }
> .row {
display: grid;
grid-template-columns: auto 1fr;
align-items: center;
gap: 0.5rem;
span.material-symbols-outlined {
font-size: 1.5rem;
color: #222;
}
input[type='text'],
input[type='password'] {
width: 100%;
min-height: 2.5rem;
padding-left: 0.5rem;
}
}
.right { .right {
justify-self: end; justify-self: end;
} }
@ -226,6 +264,12 @@ Controls - for things like buttons, input, select
.center { .center {
justify-self: center; justify-self: center;
} }
@media screen and (max-width: 1024px) {
min-width: 0;
padding: 1.5rem;
gap: 1.5rem;
}
} }
details { details {

@ -13,6 +13,10 @@
min-height: calc(100vh - 10rem); min-height: calc(100vh - 10rem);
} }
/* display: flex;
flex-direction: column;
align-items: center; */
& { & {
display: grid; display: grid;
grid-auto-flow: row; grid-auto-flow: row;
@ -164,10 +168,6 @@
color: #000d; color: #000d;
} }
} }
.card {
grid-template-rows: auto auto 1fr;
}
} }
section.projects { section.projects {

@ -0,0 +1,17 @@
@layer page {
main {
justify-self: center;
display: grid;
place-items: center;
place-content: center;
padding: 4.5rem 3rem;
gap: 4.5rem;
@media screen and (max-width: 1024px) {
padding: 3rem 1.5rem;
gap: 3rem;
}
}
}

@ -146,8 +146,7 @@
} }
} }
img, img {
video {
display: block; display: block;
margin: 0 auto; margin: 0 auto;
@ -157,7 +156,7 @@
border: 2px solid #333; border: 2px solid #333;
border-radius: 0.25rem; border-radius: 0.25rem;
box-shadow: 0.25rem 0.25rem 0 0 #333; box-shadow: 0.25rem 0.25rem 0 0 #333;
background: #000; /* background: color-mix(in lab, var(--card-base-internal, #ededed), #fff 20%); */
&.fill { &.fill {
width: 100%; width: 100%;
@ -174,10 +173,6 @@
} }
} }
video {
margin: 1rem auto;
}
p:first-child { p:first-child {
margin-top: 0; margin-top: 0;
} }

Loading…
Cancel
Save