Compare commits

..

4 commits

72 changed files with 17678 additions and 260 deletions

View file

@ -10,13 +10,15 @@
},
"features": {
"ghcr.io/devcontainers/features/node:1": {},
},
"ghcr.io/astronomer/devcontainer-features/astro-cli:1": {},
"ghcr.io/cirolosapio/devcontainers-features/alpine-git:0": {}
}
// Features to add to the dev container. More info: https://containers.dev/features.
// "features": {},
// Use 'forwardPorts' to make a list of ports inside the container available locally.
"forwardPorts": [4321]
// "forwardPorts": [],
// Uncomment the next line to run commands after the container is created.
// "postCreateCommand": "cat /etc/os-release",

View file

@ -5,10 +5,6 @@ on:
branches:
- "main"
defaults:
run:
working-directory: /home/mom/git/eleboog-astro
jobs:
builder:
runs-on: ubuntu-latest

3
.gitignore vendored
View file

@ -12,13 +12,10 @@ yarn-debug.log*
yarn-error.log*
pnpm-debug.log*
# environment variables
.env
.env.production
.pnpm-store
# macOS-specific files
.DS_Store

10
.vscode/settings.json vendored Normal file
View file

@ -0,0 +1,10 @@
{
"inlineChat.lineNaturalLanguageHint": false,
"workbench.editor.empty.hint": "hidden",
"github.copilot.enable": {
"*": false,
"plaintext": false,
"markdown": false,
"scminput": false
}
}

View file

@ -1,54 +1,79 @@
# okay, let's figure this out
# syntax=docker/dockerfile:1
FROM node:23.11-slim AS base
# Comments are provided throughout this file to help you get started.
# If you need more help, visit the Dockerfile reference guide at
# https://docs.docker.com/go/dockerfile-reference/
# Want to help us make this template better? Share your feedback here: https://forms.gle/ybq9Krt8jtBL3iCk7
ARG NODE_VERSION=23.3.0
ARG PNPM_VERSION=9.10.0
################################################################################
# Use node image for base image for all stages.
FROM node:${NODE_VERSION}-alpine AS base
# Set working directory for all build stages.
WORKDIR /usr/src/app
# Install pnpm.
RUN --mount=type=cache,target=/root/.npm \
npm install -g pnpm@${PNPM_VERSION}
ENV PNPM_HOME="/pnpm"
ENV PATH="$PNPM_HOME:$PATH"
RUN corepack enable
WORKDIR /app
################################################################################
# Create a stage for installing production dependecies.
FROM base AS deps
# By copying only the package.json and package-lock.json here, we ensure that the following `-deps` steps are independent of the source code.
# Therefore, the `-deps` steps will be skipped if only the source code changes.
COPY package.json pnpm-lock.yaml ./
# Download dependencies as a separate step to take advantage of Docker's caching.
# Leverage a cache mount to /root/.local/share/pnpm/store to speed up subsequent builds.
# Leverage bind mounts to package.json and pnpm-lock.yaml to avoid having to copy them
# into this layer.
RUN --mount=type=bind,source=package.json,target=package.json \
--mount=type=bind,source=pnpm-lock.yaml,target=pnpm-lock.yaml \
--mount=type=cache,target=/root/.local/share/pnpm/store \
pnpm install --prod --frozen-lockfile
FROM base AS prod-deps
RUN --mount=type=cache,id=pnpm,target=/pnpm/store pnpm install --prod --frozen-lockfile
################################################################################
# Create a stage for building the application.
FROM deps AS build
FROM base AS build-deps
RUN --mount=type=cache,id=pnpm,target=/pnpm/store pnpm install --frozen-lockfile
# Download additional development dependencies before building, as some projects require
# "devDependencies" to be installed to build. If you don't need this, remove this step.
RUN --mount=type=bind,source=package.json,target=package.json \
--mount=type=bind,source=pnpm-lock.yaml,target=pnpm-lock.yaml \
--mount=type=cache,target=/root/.local/share/pnpm/store \
pnpm install --frozen-lockfile
FROM build-deps AS build
# Copy the rest of the source files into the image.
COPY . .
# Run the build script.
RUN pnpm run build
FROM base AS runtime
# Copy dependencies
COPY --from=prod-deps /app/node_modules ./node_modules
# Copy the built output
COPY --from=build /app/dist ./dist
################################################################################
# Create a new stage to run the application with minimal runtime dependencies
# where the necessary files are copied from the build stage.
FROM base AS final
# Bind to all interfaces
# Use production node environment by default.
ENV NODE_ENV=production
# Run the application as a non-root user.
USER node
# Copy package.json so that package manager commands can be used.
COPY package.json .
# Copy the production dependencies from the deps stage and also
# the built application from the build stage into the image.
COPY --from=deps /usr/src/app/node_modules ./node_modules
COPY --from=build /usr/src/app/dist ./dist
# Expose the port that the application listens on.
ENV HOST=0.0.0.0
# Port to listen on
ENV PORT=4321
# Just convention, not required
EXPOSE 4321
# Start the app
CMD ["node", "./dist/server/entry.mjs"]
# Install NGINX
RUN apt-get update && apt-get install -y nginx && rm -rf /var/lib/apt/lists/*
# Copy NGINX config
COPY nginx.conf /etc/nginx/nginx.conf
# Serve static files from /app/dist/client
RUN mkdir -p /var/www/html && ln -s /app/dist/client /var/www/html
# Expose NGINX port
EXPOSE 8080
# Start both NGINX and Node server
CMD service nginx start && node ./dist/server/entry.mjs
# Run the application.
CMD pnpm start

View file

@ -23,6 +23,8 @@ const m2dxOptions = {
export default defineConfig({
site: "https://eleboog.com",
output: 'server',
vite: {
ssr: {
external: ['prismjs'],
@ -42,8 +44,6 @@ export default defineConfig({
'/now': '/journal#now',
'/links': '/sharefeed',
'/ideas': '/me#ideas',
'/chipotle': '/me#chipotle',
'/ai': '/me#ai',
'/feed.xml': '/feeds/feed.xml',
'/feeds': '/feeds/feed.xml',
},
@ -54,8 +54,6 @@ export default defineConfig({
rehypePlugins: [rehypeSlug, [rehypeMdxCodeProps, {tagName: 'code'}]],
}), icon()],
output: 'server',
adapter: node({
mode: 'standalone'
})

View file

@ -0,0 +1,59 @@
alter table "user" add column "emailVerified" boolean not null;
alter table "user" add column "createdAt" timestamp not null;
alter table "user" add column "updatedAt" timestamp not null;
alter table "user" add column "username" text unique;
alter table "user" add column "displayUsername" text;
alter table "user" add column "role" text;
alter table "user" add column "banned" boolean;
alter table "user" add column "banReason" text;
alter table "user" add column "banExpires" timestamp;
alter table "session" add column "expiresAt" timestamp not null;
alter table "session" add column "createdAt" timestamp not null;
alter table "session" add column "updatedAt" timestamp not null;
alter table "session" add column "ipAddress" text;
alter table "session" add column "userAgent" text;
alter table "session" add column "userId" text not null references "user" ("id");
alter table "session" add column "impersonatedBy" text;
alter table "account" add column "accountId" text not null;
alter table "account" add column "providerId" text not null;
alter table "account" add column "userId" text not null references "user" ("id");
alter table "account" add column "accessToken" text;
alter table "account" add column "refreshToken" text;
alter table "account" add column "idToken" text;
alter table "account" add column "accessTokenExpiresAt" timestamp;
alter table "account" add column "refreshTokenExpiresAt" timestamp;
alter table "account" add column "createdAt" timestamp not null;
alter table "account" add column "updatedAt" timestamp not null;
alter table "verification" add column "expiresAt" timestamp not null;
alter table "verification" add column "createdAt" timestamp;
alter table "verification" add column "updatedAt" timestamp;
create table "passkey" ("id" text not null primary key, "name" text, "publicKey" text not null, "userId" text not null references "user" ("id"), "credentialID" text not null, "counter" integer not null, "deviceType" text not null, "backedUp" boolean not null, "transports" text, "createdAt" timestamp);

View file

@ -0,0 +1 @@
;

View file

@ -0,0 +1,9 @@
create table "user" ("id" text not null primary key, "name" text not null, "email" text not null unique, "emailVerified" boolean not null, "image" text, "createdAt" timestamp not null, "updatedAt" timestamp not null, "username" text unique, "displayUsername" text, "role" text, "banned" boolean, "banReason" text, "banExpires" timestamp);
create table "session" ("id" text not null primary key, "expiresAt" timestamp not null, "token" text not null unique, "createdAt" timestamp not null, "updatedAt" timestamp not null, "ipAddress" text, "userAgent" text, "userId" text not null references "user" ("id"), "impersonatedBy" text);
create table "account" ("id" text not null primary key, "accountId" text not null, "providerId" text not null, "userId" text not null references "user" ("id"), "accessToken" text, "refreshToken" text, "idToken" text, "accessTokenExpiresAt" timestamp, "refreshTokenExpiresAt" timestamp, "scope" text, "password" text, "createdAt" timestamp not null, "updatedAt" timestamp not null);
create table "verification" ("id" text not null primary key, "identifier" text not null, "value" text not null, "expiresAt" timestamp not null, "createdAt" timestamp, "updatedAt" timestamp);
create table "passkey" ("id" text not null primary key, "name" text, "publicKey" text not null, "userId" text not null references "user" ("id"), "credentialID" text not null, "counter" integer not null, "deviceType" text not null, "backedUp" boolean not null, "transports" text, "createdAt" timestamp);

View file

@ -0,0 +1 @@
;

11
drizzle.config.ts Normal file
View file

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

1
generated/prisma/client.d.ts vendored Normal file
View file

@ -0,0 +1 @@
export * from "./index"

View file

@ -0,0 +1,4 @@
/* !!! This is code generated by Prisma. Do not edit directly. !!!
/* eslint-disable */
module.exports = { ...require('.') }

1
generated/prisma/default.d.ts vendored Normal file
View file

@ -0,0 +1 @@
export * from "./index"

View file

@ -0,0 +1,4 @@
/* !!! This is code generated by Prisma. Do not edit directly. !!!
/* eslint-disable */
module.exports = { ...require('.') }

1
generated/prisma/edge.d.ts vendored Normal file
View file

@ -0,0 +1 @@
export * from "./default"

256
generated/prisma/edge.js Normal file

File diff suppressed because one or more lines are too long

View file

@ -0,0 +1,242 @@
/* !!! This is code generated by Prisma. Do not edit directly. !!!
/* eslint-disable */
Object.defineProperty(exports, "__esModule", { value: true });
const {
Decimal,
objectEnumValues,
makeStrictEnum,
Public,
getRuntime,
skip
} = require('./runtime/index-browser.js')
const Prisma = {}
exports.Prisma = Prisma
exports.$Enums = {}
/**
* Prisma Client JS version: 6.7.0
* Query Engine version: 3cff47a7f5d65c3ea74883f1d736e41d68ce91ed
*/
Prisma.prismaVersion = {
client: "6.7.0",
engine: "3cff47a7f5d65c3ea74883f1d736e41d68ce91ed"
}
Prisma.PrismaClientKnownRequestError = () => {
const runtimeName = getRuntime().prettyName;
throw new Error(`PrismaClientKnownRequestError is unable to run in this browser environment, or has been bundled for the browser (running in ${runtimeName}).
In case this error is unexpected for you, please report it in https://pris.ly/prisma-prisma-bug-report`,
)};
Prisma.PrismaClientUnknownRequestError = () => {
const runtimeName = getRuntime().prettyName;
throw new Error(`PrismaClientUnknownRequestError is unable to run in this browser environment, or has been bundled for the browser (running in ${runtimeName}).
In case this error is unexpected for you, please report it in https://pris.ly/prisma-prisma-bug-report`,
)}
Prisma.PrismaClientRustPanicError = () => {
const runtimeName = getRuntime().prettyName;
throw new Error(`PrismaClientRustPanicError is unable to run in this browser environment, or has been bundled for the browser (running in ${runtimeName}).
In case this error is unexpected for you, please report it in https://pris.ly/prisma-prisma-bug-report`,
)}
Prisma.PrismaClientInitializationError = () => {
const runtimeName = getRuntime().prettyName;
throw new Error(`PrismaClientInitializationError is unable to run in this browser environment, or has been bundled for the browser (running in ${runtimeName}).
In case this error is unexpected for you, please report it in https://pris.ly/prisma-prisma-bug-report`,
)}
Prisma.PrismaClientValidationError = () => {
const runtimeName = getRuntime().prettyName;
throw new Error(`PrismaClientValidationError is unable to run in this browser environment, or has been bundled for the browser (running in ${runtimeName}).
In case this error is unexpected for you, please report it in https://pris.ly/prisma-prisma-bug-report`,
)}
Prisma.Decimal = Decimal
/**
* Re-export of sql-template-tag
*/
Prisma.sql = () => {
const runtimeName = getRuntime().prettyName;
throw new Error(`sqltag is unable to run in this browser environment, or has been bundled for the browser (running in ${runtimeName}).
In case this error is unexpected for you, please report it in https://pris.ly/prisma-prisma-bug-report`,
)}
Prisma.empty = () => {
const runtimeName = getRuntime().prettyName;
throw new Error(`empty is unable to run in this browser environment, or has been bundled for the browser (running in ${runtimeName}).
In case this error is unexpected for you, please report it in https://pris.ly/prisma-prisma-bug-report`,
)}
Prisma.join = () => {
const runtimeName = getRuntime().prettyName;
throw new Error(`join is unable to run in this browser environment, or has been bundled for the browser (running in ${runtimeName}).
In case this error is unexpected for you, please report it in https://pris.ly/prisma-prisma-bug-report`,
)}
Prisma.raw = () => {
const runtimeName = getRuntime().prettyName;
throw new Error(`raw is unable to run in this browser environment, or has been bundled for the browser (running in ${runtimeName}).
In case this error is unexpected for you, please report it in https://pris.ly/prisma-prisma-bug-report`,
)}
Prisma.validator = Public.validator
/**
* Extensions
*/
Prisma.getExtensionContext = () => {
const runtimeName = getRuntime().prettyName;
throw new Error(`Extensions.getExtensionContext is unable to run in this browser environment, or has been bundled for the browser (running in ${runtimeName}).
In case this error is unexpected for you, please report it in https://pris.ly/prisma-prisma-bug-report`,
)}
Prisma.defineExtension = () => {
const runtimeName = getRuntime().prettyName;
throw new Error(`Extensions.defineExtension is unable to run in this browser environment, or has been bundled for the browser (running in ${runtimeName}).
In case this error is unexpected for you, please report it in https://pris.ly/prisma-prisma-bug-report`,
)}
/**
* Shorthand utilities for JSON filtering
*/
Prisma.DbNull = objectEnumValues.instances.DbNull
Prisma.JsonNull = objectEnumValues.instances.JsonNull
Prisma.AnyNull = objectEnumValues.instances.AnyNull
Prisma.NullTypes = {
DbNull: objectEnumValues.classes.DbNull,
JsonNull: objectEnumValues.classes.JsonNull,
AnyNull: objectEnumValues.classes.AnyNull
}
/**
* Enums
*/
exports.Prisma.TransactionIsolationLevel = makeStrictEnum({
ReadUncommitted: 'ReadUncommitted',
ReadCommitted: 'ReadCommitted',
RepeatableRead: 'RepeatableRead',
Serializable: 'Serializable'
});
exports.Prisma.AccountScalarFieldEnum = {
id: 'id',
accountId: 'accountId',
providerId: 'providerId',
userId: 'userId',
accessToken: 'accessToken',
refreshToken: 'refreshToken',
idToken: 'idToken',
accessTokenExpiresAt: 'accessTokenExpiresAt',
refreshTokenExpiresAt: 'refreshTokenExpiresAt',
scope: 'scope',
password: 'password',
createdAt: 'createdAt',
updatedAt: 'updatedAt'
};
exports.Prisma.PasskeyScalarFieldEnum = {
id: 'id',
name: 'name',
publicKey: 'publicKey',
userId: 'userId',
credentialID: 'credentialID',
counter: 'counter',
deviceType: 'deviceType',
backedUp: 'backedUp',
transports: 'transports',
createdAt: 'createdAt'
};
exports.Prisma.SessionScalarFieldEnum = {
id: 'id',
expiresAt: 'expiresAt',
token: 'token',
createdAt: 'createdAt',
updatedAt: 'updatedAt',
ipAddress: 'ipAddress',
userAgent: 'userAgent',
userId: 'userId',
impersonatedBy: 'impersonatedBy'
};
exports.Prisma.UserScalarFieldEnum = {
id: 'id',
name: 'name',
email: 'email',
emailVerified: 'emailVerified',
image: 'image',
createdAt: 'createdAt',
updatedAt: 'updatedAt',
username: 'username',
displayUsername: 'displayUsername',
role: 'role',
banned: 'banned',
banReason: 'banReason',
banExpires: 'banExpires'
};
exports.Prisma.VerificationScalarFieldEnum = {
id: 'id',
identifier: 'identifier',
value: 'value',
expiresAt: 'expiresAt',
createdAt: 'createdAt',
updatedAt: 'updatedAt'
};
exports.Prisma.SortOrder = {
asc: 'asc',
desc: 'desc'
};
exports.Prisma.QueryMode = {
default: 'default',
insensitive: 'insensitive'
};
exports.Prisma.NullsOrder = {
first: 'first',
last: 'last'
};
exports.Prisma.ModelName = {
account: 'account',
passkey: 'passkey',
session: 'session',
user: 'user',
verification: 'verification'
};
/**
* This is a stub Prisma Client that will error at runtime if called.
*/
class PrismaClient {
constructor() {
return new Proxy(this, {
get(target, prop) {
let message
const runtime = getRuntime()
if (runtime.isEdge) {
message = `PrismaClient is not configured to run in ${runtime.prettyName}. In order to run Prisma Client on edge runtime, either:
- Use Prisma Accelerate: https://pris.ly/d/accelerate
- Use Driver Adapters: https://pris.ly/d/driver-adapters
`;
} else {
message = 'PrismaClient is unable to run in this browser environment, or has been bundled for the browser (running in `' + runtime.prettyName + '`).'
}
message += `
If this is unexpected, please open an issue: https://pris.ly/prisma-prisma-bug-report`
throw new Error(message)
}
})
}
}
exports.PrismaClient = PrismaClient
Object.assign(exports, Prisma)

9458
generated/prisma/index.d.ts vendored Normal file

File diff suppressed because it is too large Load diff

277
generated/prisma/index.js Normal file

File diff suppressed because one or more lines are too long

Binary file not shown.

View file

@ -0,0 +1,140 @@
{
"name": "prisma-client-d2892a00f3a6572ec0e53630203b0cd2844b8ae4e3b7e0dcd9321aeee4f0ca5e",
"main": "index.js",
"types": "index.d.ts",
"browser": "index-browser.js",
"exports": {
"./client": {
"require": {
"node": "./index.js",
"edge-light": "./wasm.js",
"workerd": "./wasm.js",
"worker": "./wasm.js",
"browser": "./index-browser.js",
"default": "./index.js"
},
"import": {
"node": "./index.js",
"edge-light": "./wasm.js",
"workerd": "./wasm.js",
"worker": "./wasm.js",
"browser": "./index-browser.js",
"default": "./index.js"
},
"default": "./index.js"
},
"./package.json": "./package.json",
".": {
"require": {
"node": "./index.js",
"edge-light": "./wasm.js",
"workerd": "./wasm.js",
"worker": "./wasm.js",
"browser": "./index-browser.js",
"default": "./index.js"
},
"import": {
"node": "./index.js",
"edge-light": "./wasm.js",
"workerd": "./wasm.js",
"worker": "./wasm.js",
"browser": "./index-browser.js",
"default": "./index.js"
},
"default": "./index.js"
},
"./edge": {
"types": "./edge.d.ts",
"require": "./edge.js",
"import": "./edge.js",
"default": "./edge.js"
},
"./react-native": {
"types": "./react-native.d.ts",
"require": "./react-native.js",
"import": "./react-native.js",
"default": "./react-native.js"
},
"./extension": {
"types": "./extension.d.ts",
"require": "./extension.js",
"import": "./extension.js",
"default": "./extension.js"
},
"./index-browser": {
"types": "./index.d.ts",
"require": "./index-browser.js",
"import": "./index-browser.js",
"default": "./index-browser.js"
},
"./index": {
"types": "./index.d.ts",
"require": "./index.js",
"import": "./index.js",
"default": "./index.js"
},
"./wasm": {
"types": "./wasm.d.ts",
"require": "./wasm.js",
"import": "./wasm.mjs",
"default": "./wasm.mjs"
},
"./runtime/client": {
"types": "./runtime/client.d.ts",
"require": "./runtime/client.js",
"import": "./runtime/client.mjs",
"default": "./runtime/client.mjs"
},
"./runtime/library": {
"types": "./runtime/library.d.ts",
"require": "./runtime/library.js",
"import": "./runtime/library.mjs",
"default": "./runtime/library.mjs"
},
"./runtime/binary": {
"types": "./runtime/binary.d.ts",
"require": "./runtime/binary.js",
"import": "./runtime/binary.mjs",
"default": "./runtime/binary.mjs"
},
"./runtime/wasm": {
"types": "./runtime/wasm.d.ts",
"require": "./runtime/wasm.js",
"import": "./runtime/wasm.mjs",
"default": "./runtime/wasm.mjs"
},
"./runtime/edge": {
"types": "./runtime/edge.d.ts",
"require": "./runtime/edge.js",
"import": "./runtime/edge-esm.js",
"default": "./runtime/edge-esm.js"
},
"./runtime/react-native": {
"types": "./runtime/react-native.d.ts",
"require": "./runtime/react-native.js",
"import": "./runtime/react-native.js",
"default": "./runtime/react-native.js"
},
"./generator-build": {
"require": "./generator-build/index.js",
"import": "./generator-build/index.js",
"default": "./generator-build/index.js"
},
"./sql": {
"require": {
"types": "./sql.d.ts",
"node": "./sql.js",
"default": "./sql.js"
},
"import": {
"types": "./sql.d.ts",
"node": "./sql.mjs",
"default": "./sql.mjs"
},
"default": "./sql.js"
},
"./*": "./*"
},
"version": "6.7.0",
"sideEffects": false
}

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

View file

@ -0,0 +1,370 @@
declare class AnyNull extends NullTypesEnumValue {
#private;
}
declare type Args<T, F extends Operation> = T extends {
[K: symbol]: {
types: {
operations: {
[K in F]: {
args: any;
};
};
};
};
} ? T[symbol]['types']['operations'][F]['args'] : any;
declare class DbNull extends NullTypesEnumValue {
#private;
}
export declare function Decimal(n: Decimal.Value): Decimal;
export declare namespace Decimal {
export type Constructor = typeof Decimal;
export type Instance = Decimal;
export type Rounding = 0 | 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8;
export type Modulo = Rounding | 9;
export type Value = string | number | Decimal;
// http://mikemcl.github.io/decimal.js/#constructor-properties
export interface Config {
precision?: number;
rounding?: Rounding;
toExpNeg?: number;
toExpPos?: number;
minE?: number;
maxE?: number;
crypto?: boolean;
modulo?: Modulo;
defaults?: boolean;
}
}
export declare class Decimal {
readonly d: number[];
readonly e: number;
readonly s: number;
constructor(n: Decimal.Value);
absoluteValue(): Decimal;
abs(): Decimal;
ceil(): Decimal;
clampedTo(min: Decimal.Value, max: Decimal.Value): Decimal;
clamp(min: Decimal.Value, max: Decimal.Value): Decimal;
comparedTo(n: Decimal.Value): number;
cmp(n: Decimal.Value): number;
cosine(): Decimal;
cos(): Decimal;
cubeRoot(): Decimal;
cbrt(): Decimal;
decimalPlaces(): number;
dp(): number;
dividedBy(n: Decimal.Value): Decimal;
div(n: Decimal.Value): Decimal;
dividedToIntegerBy(n: Decimal.Value): Decimal;
divToInt(n: Decimal.Value): Decimal;
equals(n: Decimal.Value): boolean;
eq(n: Decimal.Value): boolean;
floor(): Decimal;
greaterThan(n: Decimal.Value): boolean;
gt(n: Decimal.Value): boolean;
greaterThanOrEqualTo(n: Decimal.Value): boolean;
gte(n: Decimal.Value): boolean;
hyperbolicCosine(): Decimal;
cosh(): Decimal;
hyperbolicSine(): Decimal;
sinh(): Decimal;
hyperbolicTangent(): Decimal;
tanh(): Decimal;
inverseCosine(): Decimal;
acos(): Decimal;
inverseHyperbolicCosine(): Decimal;
acosh(): Decimal;
inverseHyperbolicSine(): Decimal;
asinh(): Decimal;
inverseHyperbolicTangent(): Decimal;
atanh(): Decimal;
inverseSine(): Decimal;
asin(): Decimal;
inverseTangent(): Decimal;
atan(): Decimal;
isFinite(): boolean;
isInteger(): boolean;
isInt(): boolean;
isNaN(): boolean;
isNegative(): boolean;
isNeg(): boolean;
isPositive(): boolean;
isPos(): boolean;
isZero(): boolean;
lessThan(n: Decimal.Value): boolean;
lt(n: Decimal.Value): boolean;
lessThanOrEqualTo(n: Decimal.Value): boolean;
lte(n: Decimal.Value): boolean;
logarithm(n?: Decimal.Value): Decimal;
log(n?: Decimal.Value): Decimal;
minus(n: Decimal.Value): Decimal;
sub(n: Decimal.Value): Decimal;
modulo(n: Decimal.Value): Decimal;
mod(n: Decimal.Value): Decimal;
naturalExponential(): Decimal;
exp(): Decimal;
naturalLogarithm(): Decimal;
ln(): Decimal;
negated(): Decimal;
neg(): Decimal;
plus(n: Decimal.Value): Decimal;
add(n: Decimal.Value): Decimal;
precision(includeZeros?: boolean): number;
sd(includeZeros?: boolean): number;
round(): Decimal;
sine() : Decimal;
sin() : Decimal;
squareRoot(): Decimal;
sqrt(): Decimal;
tangent() : Decimal;
tan() : Decimal;
times(n: Decimal.Value): Decimal;
mul(n: Decimal.Value) : Decimal;
toBinary(significantDigits?: number): string;
toBinary(significantDigits: number, rounding: Decimal.Rounding): string;
toDecimalPlaces(decimalPlaces?: number): Decimal;
toDecimalPlaces(decimalPlaces: number, rounding: Decimal.Rounding): Decimal;
toDP(decimalPlaces?: number): Decimal;
toDP(decimalPlaces: number, rounding: Decimal.Rounding): Decimal;
toExponential(decimalPlaces?: number): string;
toExponential(decimalPlaces: number, rounding: Decimal.Rounding): string;
toFixed(decimalPlaces?: number): string;
toFixed(decimalPlaces: number, rounding: Decimal.Rounding): string;
toFraction(max_denominator?: Decimal.Value): Decimal[];
toHexadecimal(significantDigits?: number): string;
toHexadecimal(significantDigits: number, rounding: Decimal.Rounding): string;
toHex(significantDigits?: number): string;
toHex(significantDigits: number, rounding?: Decimal.Rounding): string;
toJSON(): string;
toNearest(n: Decimal.Value, rounding?: Decimal.Rounding): Decimal;
toNumber(): number;
toOctal(significantDigits?: number): string;
toOctal(significantDigits: number, rounding: Decimal.Rounding): string;
toPower(n: Decimal.Value): Decimal;
pow(n: Decimal.Value): Decimal;
toPrecision(significantDigits?: number): string;
toPrecision(significantDigits: number, rounding: Decimal.Rounding): string;
toSignificantDigits(significantDigits?: number): Decimal;
toSignificantDigits(significantDigits: number, rounding: Decimal.Rounding): Decimal;
toSD(significantDigits?: number): Decimal;
toSD(significantDigits: number, rounding: Decimal.Rounding): Decimal;
toString(): string;
truncated(): Decimal;
trunc(): Decimal;
valueOf(): string;
static abs(n: Decimal.Value): Decimal;
static acos(n: Decimal.Value): Decimal;
static acosh(n: Decimal.Value): Decimal;
static add(x: Decimal.Value, y: Decimal.Value): Decimal;
static asin(n: Decimal.Value): Decimal;
static asinh(n: Decimal.Value): Decimal;
static atan(n: Decimal.Value): Decimal;
static atanh(n: Decimal.Value): Decimal;
static atan2(y: Decimal.Value, x: Decimal.Value): Decimal;
static cbrt(n: Decimal.Value): Decimal;
static ceil(n: Decimal.Value): Decimal;
static clamp(n: Decimal.Value, min: Decimal.Value, max: Decimal.Value): Decimal;
static clone(object?: Decimal.Config): Decimal.Constructor;
static config(object: Decimal.Config): Decimal.Constructor;
static cos(n: Decimal.Value): Decimal;
static cosh(n: Decimal.Value): Decimal;
static div(x: Decimal.Value, y: Decimal.Value): Decimal;
static exp(n: Decimal.Value): Decimal;
static floor(n: Decimal.Value): Decimal;
static hypot(...n: Decimal.Value[]): Decimal;
static isDecimal(object: any): object is Decimal;
static ln(n: Decimal.Value): Decimal;
static log(n: Decimal.Value, base?: Decimal.Value): Decimal;
static log2(n: Decimal.Value): Decimal;
static log10(n: Decimal.Value): Decimal;
static max(...n: Decimal.Value[]): Decimal;
static min(...n: Decimal.Value[]): Decimal;
static mod(x: Decimal.Value, y: Decimal.Value): Decimal;
static mul(x: Decimal.Value, y: Decimal.Value): Decimal;
static noConflict(): Decimal.Constructor; // Browser only
static pow(base: Decimal.Value, exponent: Decimal.Value): Decimal;
static random(significantDigits?: number): Decimal;
static round(n: Decimal.Value): Decimal;
static set(object: Decimal.Config): Decimal.Constructor;
static sign(n: Decimal.Value): number;
static sin(n: Decimal.Value): Decimal;
static sinh(n: Decimal.Value): Decimal;
static sqrt(n: Decimal.Value): Decimal;
static sub(x: Decimal.Value, y: Decimal.Value): Decimal;
static sum(...n: Decimal.Value[]): Decimal;
static tan(n: Decimal.Value): Decimal;
static tanh(n: Decimal.Value): Decimal;
static trunc(n: Decimal.Value): Decimal;
static readonly default?: Decimal.Constructor;
static readonly Decimal?: Decimal.Constructor;
static readonly precision: number;
static readonly rounding: Decimal.Rounding;
static readonly toExpNeg: number;
static readonly toExpPos: number;
static readonly minE: number;
static readonly maxE: number;
static readonly crypto: boolean;
static readonly modulo: Decimal.Modulo;
static readonly ROUND_UP: 0;
static readonly ROUND_DOWN: 1;
static readonly ROUND_CEIL: 2;
static readonly ROUND_FLOOR: 3;
static readonly ROUND_HALF_UP: 4;
static readonly ROUND_HALF_DOWN: 5;
static readonly ROUND_HALF_EVEN: 6;
static readonly ROUND_HALF_CEIL: 7;
static readonly ROUND_HALF_FLOOR: 8;
static readonly EUCLID: 9;
}
declare type Exact<A, W> = (A extends unknown ? (W extends A ? {
[K in keyof A]: Exact<A[K], W[K]>;
} : W) : never) | (A extends Narrowable ? A : never);
export declare function getRuntime(): GetRuntimeOutput;
declare type GetRuntimeOutput = {
id: RuntimeName;
prettyName: string;
isEdge: boolean;
};
declare class JsonNull extends NullTypesEnumValue {
#private;
}
/**
* Generates more strict variant of an enum which, unlike regular enum,
* throws on non-existing property access. This can be useful in following situations:
* - we have an API, that accepts both `undefined` and `SomeEnumType` as an input
* - enum values are generated dynamically from DMMF.
*
* In that case, if using normal enums and no compile-time typechecking, using non-existing property
* will result in `undefined` value being used, which will be accepted. Using strict enum
* in this case will help to have a runtime exception, telling you that you are probably doing something wrong.
*
* Note: if you need to check for existence of a value in the enum you can still use either
* `in` operator or `hasOwnProperty` function.
*
* @param definition
* @returns
*/
export declare function makeStrictEnum<T extends Record<PropertyKey, string | number>>(definition: T): T;
declare type Narrowable = string | number | bigint | boolean | [];
declare class NullTypesEnumValue extends ObjectEnumValue {
_getNamespace(): string;
}
/**
* Base class for unique values of object-valued enums.
*/
declare abstract class ObjectEnumValue {
constructor(arg?: symbol);
abstract _getNamespace(): string;
_getName(): string;
toString(): string;
}
export declare const objectEnumValues: {
classes: {
DbNull: typeof DbNull;
JsonNull: typeof JsonNull;
AnyNull: typeof AnyNull;
};
instances: {
DbNull: DbNull;
JsonNull: JsonNull;
AnyNull: AnyNull;
};
};
declare type Operation = 'findFirst' | 'findFirstOrThrow' | 'findUnique' | 'findUniqueOrThrow' | 'findMany' | 'create' | 'createMany' | 'createManyAndReturn' | 'update' | 'updateMany' | 'updateManyAndReturn' | 'upsert' | 'delete' | 'deleteMany' | 'aggregate' | 'count' | 'groupBy' | '$queryRaw' | '$executeRaw' | '$queryRawUnsafe' | '$executeRawUnsafe' | 'findRaw' | 'aggregateRaw' | '$runCommandRaw';
declare namespace Public {
export {
validator
}
}
export { Public }
declare type RuntimeName = 'workerd' | 'deno' | 'netlify' | 'node' | 'bun' | 'edge-light' | '';
declare function validator<V>(): <S>(select: Exact<S, V>) => S;
declare function validator<C, M extends Exclude<keyof C, `$${string}`>, O extends keyof C[M] & Operation>(client: C, model: M, operation: O): <S>(select: Exact<S, Args<C[M], O>>) => S;
declare function validator<C, M extends Exclude<keyof C, `$${string}`>, O extends keyof C[M] & Operation, P extends keyof Args<C[M], O>>(client: C, model: M, operation: O, prop: P): <S>(select: Exact<S, Args<C[M], O>[P]>) => S;
export { }

File diff suppressed because one or more lines are too long

3647
generated/prisma/runtime/library.d.ts vendored Normal file

File diff suppressed because it is too large Load diff

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

View file

@ -0,0 +1,82 @@
generator client {
provider = "prisma-client-js"
output = "../generated/prisma"
}
datasource db {
provider = "postgresql"
url = env("DATABASE_URL")
directUrl = env("DIRECT_URL")
}
model account {
id String @id
accountId String
providerId String
userId String
accessToken String?
refreshToken String?
idToken String?
accessTokenExpiresAt DateTime? @db.Timestamp(6)
refreshTokenExpiresAt DateTime? @db.Timestamp(6)
scope String?
password String?
createdAt DateTime @db.Timestamp(6)
updatedAt DateTime @db.Timestamp(6)
user user @relation(fields: [userId], references: [id], onDelete: NoAction, onUpdate: NoAction)
}
model passkey {
id String @id
name String?
publicKey String
userId String
credentialID String
counter Int
deviceType String
backedUp Boolean
transports String?
createdAt DateTime? @db.Timestamp(6)
user user @relation(fields: [userId], references: [id], onDelete: NoAction, onUpdate: NoAction)
}
model session {
id String @id
expiresAt DateTime @db.Timestamp(6)
token String @unique
createdAt DateTime @db.Timestamp(6)
updatedAt DateTime @db.Timestamp(6)
ipAddress String?
userAgent String?
userId String
impersonatedBy String?
user user @relation(fields: [userId], references: [id], onDelete: NoAction, onUpdate: NoAction)
}
model user {
id String @id
name String
email String @unique
emailVerified Boolean
image String?
createdAt DateTime @db.Timestamp(6)
updatedAt DateTime @db.Timestamp(6)
username String? @unique
displayUsername String?
role String?
banned Boolean?
banReason String?
banExpires DateTime? @db.Timestamp(6)
account account[]
passkey passkey[]
session session[]
}
model verification {
id String @id
identifier String
value String
expiresAt DateTime @db.Timestamp(6)
createdAt DateTime? @db.Timestamp(6)
updatedAt DateTime? @db.Timestamp(6)
}

1
generated/prisma/wasm.d.ts vendored Normal file
View file

@ -0,0 +1 @@
export * from "./index"

242
generated/prisma/wasm.js Normal file
View file

@ -0,0 +1,242 @@
/* !!! This is code generated by Prisma. Do not edit directly. !!!
/* eslint-disable */
Object.defineProperty(exports, "__esModule", { value: true });
const {
Decimal,
objectEnumValues,
makeStrictEnum,
Public,
getRuntime,
skip
} = require('./runtime/index-browser.js')
const Prisma = {}
exports.Prisma = Prisma
exports.$Enums = {}
/**
* Prisma Client JS version: 6.7.0
* Query Engine version: 3cff47a7f5d65c3ea74883f1d736e41d68ce91ed
*/
Prisma.prismaVersion = {
client: "6.7.0",
engine: "3cff47a7f5d65c3ea74883f1d736e41d68ce91ed"
}
Prisma.PrismaClientKnownRequestError = () => {
const runtimeName = getRuntime().prettyName;
throw new Error(`PrismaClientKnownRequestError is unable to run in this browser environment, or has been bundled for the browser (running in ${runtimeName}).
In case this error is unexpected for you, please report it in https://pris.ly/prisma-prisma-bug-report`,
)};
Prisma.PrismaClientUnknownRequestError = () => {
const runtimeName = getRuntime().prettyName;
throw new Error(`PrismaClientUnknownRequestError is unable to run in this browser environment, or has been bundled for the browser (running in ${runtimeName}).
In case this error is unexpected for you, please report it in https://pris.ly/prisma-prisma-bug-report`,
)}
Prisma.PrismaClientRustPanicError = () => {
const runtimeName = getRuntime().prettyName;
throw new Error(`PrismaClientRustPanicError is unable to run in this browser environment, or has been bundled for the browser (running in ${runtimeName}).
In case this error is unexpected for you, please report it in https://pris.ly/prisma-prisma-bug-report`,
)}
Prisma.PrismaClientInitializationError = () => {
const runtimeName = getRuntime().prettyName;
throw new Error(`PrismaClientInitializationError is unable to run in this browser environment, or has been bundled for the browser (running in ${runtimeName}).
In case this error is unexpected for you, please report it in https://pris.ly/prisma-prisma-bug-report`,
)}
Prisma.PrismaClientValidationError = () => {
const runtimeName = getRuntime().prettyName;
throw new Error(`PrismaClientValidationError is unable to run in this browser environment, or has been bundled for the browser (running in ${runtimeName}).
In case this error is unexpected for you, please report it in https://pris.ly/prisma-prisma-bug-report`,
)}
Prisma.Decimal = Decimal
/**
* Re-export of sql-template-tag
*/
Prisma.sql = () => {
const runtimeName = getRuntime().prettyName;
throw new Error(`sqltag is unable to run in this browser environment, or has been bundled for the browser (running in ${runtimeName}).
In case this error is unexpected for you, please report it in https://pris.ly/prisma-prisma-bug-report`,
)}
Prisma.empty = () => {
const runtimeName = getRuntime().prettyName;
throw new Error(`empty is unable to run in this browser environment, or has been bundled for the browser (running in ${runtimeName}).
In case this error is unexpected for you, please report it in https://pris.ly/prisma-prisma-bug-report`,
)}
Prisma.join = () => {
const runtimeName = getRuntime().prettyName;
throw new Error(`join is unable to run in this browser environment, or has been bundled for the browser (running in ${runtimeName}).
In case this error is unexpected for you, please report it in https://pris.ly/prisma-prisma-bug-report`,
)}
Prisma.raw = () => {
const runtimeName = getRuntime().prettyName;
throw new Error(`raw is unable to run in this browser environment, or has been bundled for the browser (running in ${runtimeName}).
In case this error is unexpected for you, please report it in https://pris.ly/prisma-prisma-bug-report`,
)}
Prisma.validator = Public.validator
/**
* Extensions
*/
Prisma.getExtensionContext = () => {
const runtimeName = getRuntime().prettyName;
throw new Error(`Extensions.getExtensionContext is unable to run in this browser environment, or has been bundled for the browser (running in ${runtimeName}).
In case this error is unexpected for you, please report it in https://pris.ly/prisma-prisma-bug-report`,
)}
Prisma.defineExtension = () => {
const runtimeName = getRuntime().prettyName;
throw new Error(`Extensions.defineExtension is unable to run in this browser environment, or has been bundled for the browser (running in ${runtimeName}).
In case this error is unexpected for you, please report it in https://pris.ly/prisma-prisma-bug-report`,
)}
/**
* Shorthand utilities for JSON filtering
*/
Prisma.DbNull = objectEnumValues.instances.DbNull
Prisma.JsonNull = objectEnumValues.instances.JsonNull
Prisma.AnyNull = objectEnumValues.instances.AnyNull
Prisma.NullTypes = {
DbNull: objectEnumValues.classes.DbNull,
JsonNull: objectEnumValues.classes.JsonNull,
AnyNull: objectEnumValues.classes.AnyNull
}
/**
* Enums
*/
exports.Prisma.TransactionIsolationLevel = makeStrictEnum({
ReadUncommitted: 'ReadUncommitted',
ReadCommitted: 'ReadCommitted',
RepeatableRead: 'RepeatableRead',
Serializable: 'Serializable'
});
exports.Prisma.AccountScalarFieldEnum = {
id: 'id',
accountId: 'accountId',
providerId: 'providerId',
userId: 'userId',
accessToken: 'accessToken',
refreshToken: 'refreshToken',
idToken: 'idToken',
accessTokenExpiresAt: 'accessTokenExpiresAt',
refreshTokenExpiresAt: 'refreshTokenExpiresAt',
scope: 'scope',
password: 'password',
createdAt: 'createdAt',
updatedAt: 'updatedAt'
};
exports.Prisma.PasskeyScalarFieldEnum = {
id: 'id',
name: 'name',
publicKey: 'publicKey',
userId: 'userId',
credentialID: 'credentialID',
counter: 'counter',
deviceType: 'deviceType',
backedUp: 'backedUp',
transports: 'transports',
createdAt: 'createdAt'
};
exports.Prisma.SessionScalarFieldEnum = {
id: 'id',
expiresAt: 'expiresAt',
token: 'token',
createdAt: 'createdAt',
updatedAt: 'updatedAt',
ipAddress: 'ipAddress',
userAgent: 'userAgent',
userId: 'userId',
impersonatedBy: 'impersonatedBy'
};
exports.Prisma.UserScalarFieldEnum = {
id: 'id',
name: 'name',
email: 'email',
emailVerified: 'emailVerified',
image: 'image',
createdAt: 'createdAt',
updatedAt: 'updatedAt',
username: 'username',
displayUsername: 'displayUsername',
role: 'role',
banned: 'banned',
banReason: 'banReason',
banExpires: 'banExpires'
};
exports.Prisma.VerificationScalarFieldEnum = {
id: 'id',
identifier: 'identifier',
value: 'value',
expiresAt: 'expiresAt',
createdAt: 'createdAt',
updatedAt: 'updatedAt'
};
exports.Prisma.SortOrder = {
asc: 'asc',
desc: 'desc'
};
exports.Prisma.QueryMode = {
default: 'default',
insensitive: 'insensitive'
};
exports.Prisma.NullsOrder = {
first: 'first',
last: 'last'
};
exports.Prisma.ModelName = {
account: 'account',
passkey: 'passkey',
session: 'session',
user: 'user',
verification: 'verification'
};
/**
* This is a stub Prisma Client that will error at runtime if called.
*/
class PrismaClient {
constructor() {
return new Proxy(this, {
get(target, prop) {
let message
const runtime = getRuntime()
if (runtime.isEdge) {
message = `PrismaClient is not configured to run in ${runtime.prettyName}. In order to run Prisma Client on edge runtime, either:
- Use Prisma Accelerate: https://pris.ly/d/accelerate
- Use Driver Adapters: https://pris.ly/d/driver-adapters
`;
} else {
message = 'PrismaClient is unable to run in this browser environment, or has been bundled for the browser (running in `' + runtime.prettyName + '`).'
}
message += `
If this is unexpected, please open an issue: https://pris.ly/prisma-prisma-bug-report`
throw new Error(message)
}
})
}
}
exports.PrismaClient = PrismaClient
Object.assign(exports, Prisma)

View file

@ -17,19 +17,30 @@
"@astrojs/react": "^4.1.6",
"@astrojs/rss": "^4.0.11",
"@astrojs/tailwind": "^5.1.5",
"@electric-sql/pglite": "^0.3.0",
"@fontsource/libre-baskerville": "^5.1.1",
"@fontsource/nunito": "^5.1.1",
"@prisma/client": "6.7.0",
"@types/lodash": "^4.17.14",
"@types/react": "^18.3.18",
"@types/react-dom": "^18.3.5",
"astro": "^5.1.8",
"astro-auto-import": "^0.4.4",
"astro-icon": "^1.1.5",
"astro-m2dx": "^0.7.16",
"astro-mdx-code-blocks": "^0.0.6",
"better-auth": "^1.2.7",
"date-fns": "^4.1.0",
"date-fns-tz": "^3.2.0",
"dotenv": "^16.5.0",
"drizzle-orm": "^0.43.1",
"feed": "^4.2.2",
"github-slugger": "^2.0.0",
"linkedom": "^0.18.6",
"lodash": "^4.17.21",
"node-html-parser": "^6.1.13",
"pg": "^8.15.6",
"postgres": "^3.4.5",
"prism": "^4.1.2",
"prism-react-renderer": "^2.4.1",
"prismjs": "^1.29.0",
@ -37,20 +48,21 @@
"react-code-block": "^1.1.1",
"react-dom": "^18.3.1",
"react-icons": "^5.4.0",
"react-qr-code": "^2.0.15",
"rehype-mdx-code-props": "^3.0.1",
"rehype-pretty-code": "^0.14.0",
"rehype-slug": "^6.0.0",
"sharp": "^0.33.5",
"tailwindcss": "^3.4.17",
"tailwindcss-animate": "^1.0.7",
"typescript": "^5.7.3",
"@types/lodash": "^4.17.14",
"astro-auto-import": "^0.4.4",
"astro-mdx-code-blocks": "^0.0.6",
"lodash": "^4.17.21"
"tailwindcss-animate": "^1.0.7"
},
"packageManager": "pnpm@9.10.0+sha512.73a29afa36a0d092ece5271de5177ecbf8318d454ecd701343131b8ebc0c1a91c487da46ab77c8e596d6acf1461e3594ced4becedf8921b074fbd8653ed7051c",
"devDependencies": {
"@types/node": "^22.15.15",
"@types/pg": "^8.15.0",
"drizzle-kit": "^0.31.1",
"prisma": "^6.7.0",
"tsx": "^4.19.4",
"typescript": "^5.7.3"
}
}

1284
pnpm-lock.yaml generated

File diff suppressed because it is too large Load diff

133
prisma/schema.prisma Normal file
View file

@ -0,0 +1,133 @@
generator client {
provider = "prisma-client-js"
output = "../node_modules/.prisma/client"
}
datasource db {
provider = "postgresql"
url = env("DATABASE_URL")
directUrl = env("DIRECT_URL")
}
// COMMENTS STUFF
model Comment {
id Int @id @default(autoincrement())
authorId String
author User @relation("Author", fields: [authorId], references: [id], onDelete: Cascade)
date DateTime
dateUpdated DateTime?
contents String
like_count Int
likes Likes[] @relation("Likes")
pinned Boolean
parentId Int
parent Comment @relation("Replies", fields: [parentId], references: [id], onDelete: Cascade)
replies Comment[] @relation("Replies")
@@map("comment")
}
model Likes {
commentId Int
comment Comment @relation("Likes", fields: [commentId], references: [id], onDelete: Cascade)
userId String
user User @relation("Likes", fields: [userId], references: [id], onDelete: Cascade)
@@id([commentId, userId])
@@map("likes")
}
// AUTH STUFF
model User {
id String @id
name String
email String
emailVerified Boolean
image String?
createdAt DateTime
updatedAt DateTime
username String?
displayUsername String?
role String?
banned Boolean?
banReason String?
banExpires DateTime?
sessions Session[]
accounts Account[]
passkeys Passkey[]
comments Comment[] @relation("Author")
likes Likes[] @relation("Likes")
@@unique([email])
@@unique([username])
@@map("user")
}
model Session {
id String @id
expiresAt DateTime
token String
createdAt DateTime
updatedAt DateTime
ipAddress String?
userAgent String?
userId String
user User @relation(fields: [userId], references: [id], onDelete: Cascade)
impersonatedBy String?
@@unique([token])
@@map("session")
}
model Account {
id String @id
accountId String
providerId String
userId String
user User @relation(fields: [userId], references: [id], onDelete: Cascade)
accessToken String?
refreshToken String?
idToken String?
accessTokenExpiresAt DateTime?
refreshTokenExpiresAt DateTime?
scope String?
password String?
createdAt DateTime
updatedAt DateTime
@@map("account")
}
model Verification {
id String @id
identifier String
value String
expiresAt DateTime
createdAt DateTime?
updatedAt DateTime?
@@map("verification")
}
model Passkey {
id String @id
name String?
publicKey String
userId String
user User @relation(fields: [userId], references: [id], onDelete: Cascade)
credentialID String
counter Int
deviceType String
backedUp Boolean
transports String?
createdAt DateTime?
@@map("passkey")
}

Binary file not shown.

Before

Width:  |  Height:  |  Size: 27 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 9.9 KiB

BIN
public/eleboog_button.jpg Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.4 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.2 KiB

31
src/actions/index.ts Normal file
View file

@ -0,0 +1,31 @@
import { defineAction, ActionError } from 'astro:actions';
import { z } from 'astro:schema';
import { authClient } from "../lib/auth-client"; //import the auth client
import { PrismaClient } from "@prisma/client";
const prisma = new PrismaClient();
export const server = {
updateUsername: defineAction({
input: z.object({
id: z.string().uuid(),
username: z.string()
.min(3, "Username too short")
.max(30, "Username too long")
.regex(/^[a-zA-Z0-9\_\.]+$/, "Illegal characters in username"),
}),
handler: async (input) => {
prisma.$connect
const user = await prisma.user.update({
where: {
id: input.id,
},
data: {
username: input.username,
}
});
return user;
}
})
}

View file

@ -0,0 +1,106 @@
---
import HR from "../components/HR.astro";
---
<style>
@import url('/src/styles/globals.css');
@tailwind components;
@layer components {
h2 {
@apply text-2xl font-serif text-title font-bold;
}
a {
@apply font-serif text-subtitle hover:underline;
}
.commentheader > a {
@apply font-mono text-lg text-subtitle hover:underline;
}
.commentfooter > a {
@apply font-mono hover:underline;
}
blockquote {
@apply p-2 pl-4 mb-2 border-l-4 border-l-crusta-300 dark:border-l-night-600 rounded-l-md bg-crusta-100 dark:bg-night-950 dark:bg-opacity-80 text-current;
}
textarea {
@apply p-2 w-full bg-crusta-100 dark:bg-night-950 dark:bg-opacity-80;
}
.textboxfooter {
@apply font-mono;
}
.textboxfooter > a {
@apply font-mono;
}
input {
@apply font-mono cursor-pointer text-subtitle hover:underline;
}
}
</style>
<div class="comments">
<HR/>
<h2 class="">Comments</h2>
<!-- <p>To leave your own comment, please <a href="/">login or register</a> your eleboog.com account.</p> -->
<form method="POST">
<label for="commentbox">Leave a comment as <a class="font-mono text-lg">kebokyo</a>&ensp;&mdash;&ensp;Markdown enabled ( <a class="font-mono text-lg">huh?</a> )</label>
<textarea id="commentbox" name="commentbox" rows="4"/>
<div class="textboxfooter">// <input type="submit" value="submit!"/> or press ctrl (cmd) + enter</div>
</form>
<p>By leaving a comment, you agree to the eleboog.com <a class="text-sm">code of conduct</a>. Dishonorable behavior may lead to account limits or termination.</p>
<HR class="border-crusta-400 dark:border-night-600 border-dashed"/>
<div class="commentheader mb-0">&starf; <a class="font-mono text-lg">kebokyo</a> (it/its) &mdash; May 4, 2025</div>
<div class="commentblock pl-2 pt-1 mb-1 border-l-4 border-crusta-500 dark:border-night-400">
<p class="mb-1">This is a markup of what I want the comments to look like.</p>
<p>Like the rest of the site, minimal visual elements & unique text formatting are used to give a text-focused approach that
can easily be scaled back for "lite" or gemini versions of the site.</p>
<div class="commentsig font-mono mb-0">
&starf; site owner of eleboog.com
</div>
<div class="commentfooter font-mono mb-2">
2 <span class="font-sans text-sm">&hearts;</span>&emsp;//&emsp;<a>reply</a> - <a>quote</a>&emsp;//&emsp;<a>edit</a> - <a class="text-red-500 dark:text-red-400">delete x</a>
</div>
</div>
<div class="commentindent pl-2 mb-2 border-l-2 border-b-2 border-gray-300 dark:border-gray-600">
<div class="commentheader mb-0">&bullet; <a style="">user</a> (they/them) &mdash; May 4, 2025</div>
<div class="commentblock pl-2 pt-1 mb-1 border-l-4 border-crusta-500 dark:border-night-400">
<p>This would be a reply to the above comment.</p>
<div class="commentsig font-mono mb-0">
&CenterDot; insert signature here
</div>
<div class="commentfooter font-mono mb-2">
3 <span class="font-sans text-sm text-crusta-500 dark:text-night-500">&hearts;</span>&check; (<a>unlike</a>)&emsp;//&emsp;<a>reply</a> - <a>quote</a>
</div>
</div>
<div class="commentindent pl-2 mb-2 border-l-2 border-b-2 border-gray-300 dark:border-gray-600">
<div class="commentheader mb-0">&bullet; <a style="">user2</a> (they/them) &mdash; May 4, 2025</div>
<div class="commentblock pl-2 pt-1 mb-1 border-l-4 border-crusta-500 dark:border-night-400">
<blockquote>
<p><i><a class="font-mono">user</a> said on May 4, 2025...</i>&emsp;(<a class="font-mono">jump to &uparrow;</a>)</p>
<p>This would be a reply to the above comment.</p>
</blockquote>
This would be an example of a "quoted" reply.
<div class="commentsig font-mono mb-0">
&CenterDot; babobee babodah
</div>
<div class="commentfooter font-mono mb-2">
2 <span class="font-sans text-sm">&hearts;</span> (<a>like?</a>)&emsp;//&emsp;<a>reply</a> - <a>quote</a>
</div>
</div>
<div class="commentindent pl-2 mb-2 border-l-2 border-b-2 border-gray-300 dark:border-gray-600">
<i>3 more replies in thread...</i>&emsp;<a class="font-mono text-lg">explore &rightarrow;</a>
</div>
<div class="commentheader mb-0">&bullet; <a style="">user3</a> (they/them) &mdash; May 4, 2025</div>
<div class="commentblock pl-2 pt-1 mb-2 border-l-4 border-b-2 border-crusta-500 dark:border-night-400">
<p>One more reply for good measure...</p>
<div class="commentsig font-mono mb-0">
&CenterDot; fdhsfklasdfijsdlfnkasfnsaf
</div>
<div class="commentfooter font-mono mb-2">
2 <span class="font-sans text-sm">&hearts;</span> (<a>like?</a>)&emsp;//&emsp;<a>reply</a> - <a>quote</a>
</div>
</div>
<i>3 more replies in thread...</i>&emsp;<a class="font-mono text-lg">explore &rightarrow;</a>
</div>
<i>7 more replies in thread...</i>&emsp;<a class="font-mono text-lg">explore &rightarrow;</a>
</div>
</div>

View file

@ -2,6 +2,8 @@
const {current} = Astro.props;
const user = Astro.locals.user;
---
<div>
@ -24,7 +26,7 @@ const {current} = Astro.props;
/
<a href='/journal' class="text-subtitle hover:underline">journal</a>
/
{/* <span class="nav-extras">
<span class="nav-extras">
<a href='/sharefeed' class="text-subtitle hover:underline">sharefeed</a>
/
<a href="/me" class="text-subtitle hover:underline">me</a>
@ -32,10 +34,19 @@ const {current} = Astro.props;
<a href="/cool" class="text-subtitle hover:underline">cool</a>
/
<a href='/feeds' class="text-subtitle hover:underline">rss</a>
</span> */}
<a href="/more" class="text-subtitle hover:underline">more</a>
</span>
<a href="/more" class="nav-more text-subtitle hover:underline">more</a>
</h2>
</div>
</div>
<!-- auth shenanigans -->
<div class="authbar mb-4 font-mono">
{Astro.locals.session ? <div>
{user?.name}&ensp;&mdash;&ensp;<a href="/account/profile" class="text-subtitle hover:underline">profile</a>
/ <a href="/account/settings" class="text-subtitle hover:underline">settings</a>
/ <button id="logout" class="text-subtitle hover:underline">logout</button>
</div> : <div>
guest&ensp;&mdash;&ensp;<a href="/account/login" class="font-serif text-sm text-subtitle hover:underline">login or register</a>
</div>}
</div>
</div>

View file

@ -12,7 +12,7 @@ export interface Props {
function getSVG(name: string) {
const filepath = `/src/svg/${name}.svg`;
const files = import.meta.glob<string>('/src/svg/**/*.svg', {
as: 'raw', eager: true,
query: '?raw', import: 'default', eager: true,
});
if (!(filepath in files)) {

View file

@ -0,0 +1,113 @@
---
// I stole ALLLLLL of this from https://webreaper.dev/posts/astro-accessible-accordion/
interface Props {
title: string;
}
const { title } = Astro.props as Props;
import HR from "../HR.astro";
---
<noscript>
<h3 class="text-lg text-title font-mono">table of contents</h3>
<slot/>
</noscript>
<div>
</div>
<div class="accordion group relative mb-2 rounded-md border border-crusta-900 dark:border-indigo-400 hidden">
<button
class="accordion__button flex w-full flex-1 items-center justify-between gap-2 p-3 text-left font-medium transition hover:text-subtitle sm:px-4"
type="button"
id={`${title} accordion menu button`}
aria-expanded="false"
aria-controls={`${title} accordion menu content`}
>
<span class="text-xl font-serif">{title}</span>
<!-- if using astro and the astro-icon package
<Icon
name="tabler:chevron-down"
aria-hidden="true"
class="accordion__chevron h-7 w-7 shrink-0 transition-transform"
/>
-->
<!-- use this is not using astro-icon (or another SVG you like) -->
<svg
class="accordion__chevron h-7 w-7 shrink-0 transition-transform"
aria-hidden="true"
xmlns="http://www.w3.org/2000/svg"
width="24"
height="24"
viewBox="0 0 24 24"
><path
fill="none"
stroke="currentColor"
stroke-linecap="round"
stroke-linejoin="round"
stroke-width="2"
d="m6 9l6 6l6-6"></path></svg
>
</button>
<div
id={`${title} accordion menu content`}
aria-labelledby={`${title} accordion menu button`}
class="accordion__content hidden max-h-0 overflow-hidden px-3 transition-all duration-300 ease-in-out sm:px-4"
>
<HR class="mt-0"/>
<div class="prose mb-4 items-center mt-1 max-w-full transition-[height]">
<slot/>
</div>
</div>
</div>
<script>
function accordionSetup() {
const menus = document.querySelectorAll(".accordion") as NodeListOf<HTMLElement>; // set this up on each menu
menus.forEach((menu) => {
menu.classList.remove("hidden");
const button = menu.querySelector(".accordion__button") as HTMLElement; // the clickable banner
const chevron = menu.querySelector(".accordion__chevron") as HTMLElement; // the chevron icon that animates
const content = menu.querySelector(".accordion__content") as HTMLElement; // the stuff that's revealed when open
if (button && content && chevron) {
button.addEventListener("click", (event) => {
if (!menu.classList.contains("active")) { // if closed, stop having it be closed!
menu.classList.add("active");
button.setAttribute("aria-expanded", "true");
// we need to set the max height to the height of the accordion object so the animations work correctly
content.classList.remove("hidden");
content.style.maxHeight = content.scrollHeight + "px";
chevron.classList.add("rotate-180");
} else { // if open, stop having it be open!!!!
menu.classList.remove("active");
button.setAttribute("aria-expanded", "false");
content.style.maxHeight = '0px';
chevron.classList.remove("rotate-180");
// make text invisible after animation
setTimeout(() => { content.classList.add("hidden") }, 300);
}
event.preventDefault();
return false;
})
}
})
}
accordionSetup();
document.addEventListener("astro:after-swap", accordionSetup);
</script>

View file

@ -0,0 +1,47 @@
---
date: 2025-03-11 13:00:00
title: A Quick Dive into Intel x86-64 Bytecode (JWL 02)
summary: Intel x86-64 assembly can be daunting just by itself... but what if you had to write *in* bytecode? Here's a brief tutorial to get you started. The second entry into my JustWriteLol series.
---
If you've ever delved into writing assembly before, you may know that it takes a *vastly* different approach than writing in most other languages,
even languages fairly close to assembly like C. Registers, memory addresses, opcodes, all of it can be pretty daunting just on their own...
But what if you had to do more than just write the instructions in text form? What if... you had to write. *every. single. byte.* of **every. single.
line. of assembly.**
You might think that this is the craziest thing you have ever heard. Who in their right mind would do such a thing? And who on the face of Planet
Earth would actually find this... fun????
Me. I'm the problem. It's me.
I've always found assembly a fun change of pace from most other programming since I took a class all about it required for my major. I already
knew about how assembly can be represented in bytecode, but I never really had to directly write it myself... until this course I'm taking now,
where the professor decided to run x86-64 assembly code in C++ like this:
```cpp
char *prog;
int value;
int p_offset = 0;
prog = (char*) mmap(0, 50000, PROT_EXEC | PROT_READ | PROT_WRITE,
MAP_PRIVATE | MAP_ANONYMOUS, -1, 0);
prog[p_offset++] = 0xb8;
prog[p_offset++] = 0x2a;
prog[p_offset++] = 0x00;
prog[p_offset++] = 0x00;
prog[p_offset++] = 0x00;
prog[p_offset++] = 0xc3;
value = (int(*)(void) prog)();
cout << value << endl;
```
```bash
$ ./a.out
42
```
<MDXImage src="https://media1.tenor.com/m/giGudNYLk_sAAAAd/benny-fallout.gif" alt='Benny from Fallout: New Vegas turning around to face you and exclaiming, in shock at your not-dead-ed-ness: "What in the goddamn...?"'/>
Yes, that's really how he wrote it. Yes, *it actually works*. Maybe Intel engineers discovered the meaning to life after all.

View file

@ -0,0 +1,159 @@
---
date: 2025-03-19 13:00:00
title: A Tale of Three Zoomies (JWL 02)
summary: Finally, another COD article. I compare the guns with the fastest movement possible in MWII, MWIII, and BO6 to try to figure out why BO6 feels so... boring.
---
quick outline
# intro
- introduce [TheXclusiveAce video](https://www.youtube.com/watch?v=bUFG9xNj-2g)
and "you could get comparable results with any SMG" comment
- explain why i want to compare this build with similar builds in MWII & III
- MWIII is easy: previous game, we all know previous game is always better ;3
- for MWII, I think a lot of what makes MWIII great is because of the
**foundation** built by MWII, so I want to bring that game into the
conversation too
- thesis statement: **the reason why BO6's weapons feel so boring compared to
previous games is entirely because of how Gunsmith works.**
- Gunsmith is why the weapons in MWIII and even MWII all feel unique
from each other, no matter if the category is under- or oversaturated
- Gunsmith is why the weapons in BO6 all feel the same and generally
boring to play with, especially if a category is oversaturated (lots of
already similar-feeling weapons) or *under*saturated (weapons don't
feel unique enough to get past the lack of choices in that category)
# methodology
- "m/s" is kinda fucking arbitrary
- we aren't gappy from jojolion and can measure the exact distances of
objects from us with our minds, the closest we can get is the ping system
and even then it isn't the most accurate
- if we were working in Source where we both a. had our speed measured in
a unit we can directly measure in-game (Hammer units) and b. already had
plentiful resources for how in-game elements are sized relative to that
unit (e. our camera is 64 Hu from the ground, so if you can't look over an
object, it's guaranteed to be 64 Hu or higher)... this would all be so
much easier... but we're on a different branch of the quake engine lmao
- so, for easier comparisons, we're going to establish some baseline movement
speeds based on certain weapons from each game
- we're looking at an AR for an overall baseline and an SMG for a
baseline specific to SMG's (since all 3 of our YYassiffied weapons are
SMG's)
- both weapons will have a default value and an "optimized" value with
movement speed attachments so we can compare the weapons both with and
without attachments (and also compare how much attachments *improve*
the weapons from base)
- BO6: XM4 for overall, C9 for SMG
- MWII: M4 for overall, MP5 for SMG
- MWIII: MTZ-556 for overall, Rival 9 for SMG
- we could use the MWII weapons... but that feels like a. a copout and
b. an unfair comparison (since those weapons were nerfed into
the ground for 90% of MWIII's lifecycle and even now have different
stats than their original MWII versions to account for 150 HP and etc.)
- MTZ-556 (CZ Bren 2) feels like the closest analogue to the M4, and
the Rival 9 (CZ Scorpion EVO 3) ditto MP5
- finally, quick disclaimer: **I do not own Black Ops 6.**
- I *do* own MWII and MWIII tho
- stats for MWIII will be taken from in-game stats when possible
- stats for MWII will be taken from XclusiveAce Gun Guides and balance
patch videos (to reflect their stats as of today)
- stats for BO6 will be taken from TrueGameData as well as XclusiveAce
videos
- for attachments, figuring out how much they help or hinder stats depends
on the game again
- MWIII is e z p z, in-game stats babyyyyy
- BO6 is also e z p z due to the nature of how Gunsmith was designed:
videos exist covering *every single attachment in a category* that
can be used as a good reference (though it should be noted that
the raw stats don't *always* stay the same for every gun, like
with rapid fire's damage range reduction sometimes not applying
to certain guns with seemingly no rhyme or reason)
- MWII will be trickier, will refer to gun guides and other content
to figure out what the attachments do
# The Weapons of Choice
(if video, make a YTP joke about the Fatboy Slim song)
(also )
## MWII: Fennec 45
- real life gun: Kriss Vector .45 ACP
- famous for the Super V recoil-mitigation system that has *only* been
accurately modeled in one video game: Insurgency
- the most notable thing about the fennec is that it has the highest sprint
speed in MWII, *even more than the combat knife*
- todo: go into MWII and test if the sprint speed is still ridonkulous
like this, including w/ the new melee's (iirc there's a melee that's
faster than the combat knife but idk for sure)
- we can further improve our movement speeds with some unique attachments
- the double tap mod actually boosts our movement speeds since it's a
smaller magazine than the base
- you can also slap on the ftac 8.5 recon barrel (not to be confused
with the ftac recon pistol based off the TEC-9 wow Infinity Ward you
are so good at naming things keep it up) and the ftac stock cap to
further boost movement speeds
- the other two slots are freebies: it's probably a good idea to
add a rear grip for improved sprint-to-fire times and other
similar stats along with the vlk 7mw lazer so you can get better
hipfire spread and not have to worry as much about ADS and recoil
and etc
## MWIII: Superi 46
- real life gun: CMMG Four-Six (Banshee)
- this isn't even a military firearm, it's a civilian gun l m f a o
- the Superi has the fastest aim walking speed @ 3.6 m/s, faster than any
other SMG in the game
- but the real reason that the Superi shines is because of its **attachments**
that allow you to elevate the gun far beyond even the melee weapons of MWIII
- bore 99 short barrel: walking +7% sprint +8% tac sprint +7%
- rescue 9 stock: walking +11% sprint +8% tac sprint +7%
- jak slash: walking +4% crouch +5% sprint +4% tac sprint +3%
- 20 round mag: ()
- we then have a freebie slot we can use for anything we want. a good
default is the quartermaster suppressor to counteract the recoil
penalties from all of our zoomy attachments, but most of the time
you're going to be running overkill with this and keeping it in your
pocket for zooming across the map, so this slot doesn't really matter
- the resulting movement speeds you get are 6.2 for base walking speed,
7.0 for regular sprint, and **8.6** for tactical sprint, which is
leagues faster than the Karambit, the fastest melee weapon in the game
## BO6: Saug
- real life gun: ....uhhhhhh
- in actuality, this gun is based off the Saug from BO4, a near-future
game where they didn't have to give a shit about modeling after real guns...
so things got a bit wacky with this one
- the saug has above average movement speeds across the board due to how
smol it is, which means it's a prime choice for further modifications
- however, the attachment selection is far more limited
- the best attachment for improving your sprint speed is actually
a **foregrip**: the Ranger Foregrip improves your sprint speed by 5% and
your tactical sprint speed by 4% (at least on SMG's)
- the *only* other attachment in the entire game that impacts your
movement speeds is the stock attachment. the stock that you put here
depends on whether you want sprint speed over aim walking speed.
- if max speed is what you need, the No Stock attachment improves
tactical sprint speeds **but not regular sprint speeds** by
around 25% and base walking speeds by 15% forwards and 20% strafing.
- if you want better aim walking speed, the Infiltrator Stock
improves your aim walking speed by 17.5% forwards and 20% strafing.
- only the no stock attachment can improve your tactical sprint speed
here; no other stock allows you to improve any sprint speeds.
- since those are the only two attachments that boost movement speed
in this game, *the other three slots are all freebies*.
- typically it's good to put on the Ergonomic Grip or the CQB Grip.
they both improve slide to fire and dive to fire times, but it's
usually better to to use the Ergonomic Grip for reasons covered
in [this video](https://www.youtube.com/watch?v=IFgJpllvEvI)
- since extended mag I *doesn't* harm our movement speeds in
BO6 (as opposed to MWII & MWIII where most extended mags will
impact movement speed in some way), it's another freebie with only
upsides for us
- the last attachment is 120% a freebie, we can add a muzzle, an
optic, barrel, rapid fire... but for this build since we're going
to be running around a lot it may be good to keep ourselves
off the radar with a suppressor
- note that **none** of these attachments are unique to the Saug specifically:
if you wanted to use this exact same build on literally any other gun in the
game, you totally could (with some specific values being slightly different ofc)
- the Saug is only chosen because it has the best *base* values out of any
other SMG: as Ace stated, you can choose *any other SMG* and get slightly
worse but still satisfactory results

View file

@ -0,0 +1,19 @@
---
title: Welp, I guess I'm the April Fool (JWL 02)
summary: Did I really think I was going to be able to do this? ...Maybe I still can.
date: 2025-04-01 13:00:00
---
Tuesday, April 4, 2025\
4:11 PM
I went to Starbucks today to attempt to get some work done. I skipped class because I woke up at 10 when my classes started at 11 and I no longer
have access to a car... so instead of doing anything productive I just stayed in bed for three hours watching YouTube and playing Balatro.
I knew I needed to break this rut, so I got out of the house and hopped on the bus to huff my things to Starbucks.
I ordered a Iced Cherry Chai because it seemed cute, and I asked for it to be put in one of those fancy glasses. They accidentally made it in a regular cup first,
so when my order was put out they asked me if I wanted both... I'm not gonna say no to free Starbucks lol. It also meant I got to see that
at least one of the employees recognized me and wrote a cute note on my to-go cup, which absolutely warmed my heart.
Finally, to really seal home the "journal entry" feel, [here's a deep cut from the oily depths of the YouTube algorithm.](https://www.youtube.com/watch?v=-psx38I_6vY)

View file

@ -0,0 +1,20 @@
---
title: We need small social medias (JWL 02)
summary: Social media shouldn't be the place where you can find everyone. It should be a place where you find a group of people you care about. A very belated entry into my JustWriteLol series.
date: 2025-04-24 13:00:00
---
I was going down an internet rabbithole when I found an interesting statement on a product's website. The product is [Letterbird](https://letterbird.co/?utm_source=lbform&utm_medium=lbformfooter),
a SaaS that lets you put a contact form for your website without having to design it yourself. The bottom of the page has a FAQ that ends with
the question, "What if I have another question or idea for a new feature?" The answer lists a group of places they can be contacted: Bluesky,
Mastodon, Threads, and X... but it also leaves this remark after those links:
> (Gosh, can we all just agree on one social media network already?)
...No? I don't really want to?
I have multiple different social medias. I have a Mastodon account, a Bluesky account, Telegram and Discord accounts
(which at this point, pretty much *are* social medias)... and they all exist for different purposes. They all exists to capture different groups
of people. And I think that is a **good** thing.
In fact,

View file

@ -1,79 +0,0 @@
---
date: 2025-06-25 20:04:00
title: A random thought about clicking a mouse (JWL 02)
summary: I had a realization about how I used my mouse while playing Call of Duty that may speak to how quirky our minds can be. The long-awaited second entry to my "please god let me make shorter blog posts" series.
---
A little bit ago, I finally reinstalled Call of Duty: Modern Warfare III (the reboot released in 2023).
I was watching YouTube videos on the game again that encouraged me to pick the game back up.
I had originally bought Modern Warfare III (which I will abbreviate as MWIII from now on)
*after* Black Ops 6 had already been out for a month or two. I heard that the game's
core multiplayer mode was surprisingly decent, so I wanted to see what I missed...
and yeah! It's one of the coolest AAA multiplayer experiences I've had so far, up there with
the extraction shooter spinoff DMZ released in 2022 and even Battlefield 4, still to this day
the G.O.A.T. of casual multiplayer gaming in my opinion.
Still, I had to be a little more self-conscious about how and when I played this game now
due to the fact that I no longer live alone &mdash; I live with multiple roommates who
might get annoyed at the click-clacking of my Razer Huntsman Mini (the one thing I don't like
about it lol). However, when I was talking with my roomates trying to confirm if they even had said issues,
the feedback was different than I expected. One of my roommates noticed me clicking my mouse multiple times
in quick succession when playing certain games, and that annoyed them *more* than my keyboard.
It took me a while to figure out what it was, but after some self-reflection,
I realized what was happening: it was my backup pistol that made me click
that way.
---
Most guns in Call of Duty have automatic fire, meaning that bullets fire
out of the gun over and over as long as you are holding down the "trigger"
(in this case, my left mouse button). In COD, you usually can only carry two
weapons: your main "primary" weapon and your backup "secondary" weapon.
Secondary weapons mostly consist of pistols that have *semi*-automatic fire.
These pistols only shoot *one* bullet when you pull the trigger, so to fire
multiple shots, you have to pull the trigger (click the mouse) multiple times, one for each shot.
That explains the pattern that my roommate was observing: since you usually have to fire
multiple shots on-target in order to down an opponent in COD, if I had my pistol out, I had
to click my mouse multiple times in rapid succession to win the fight.
But that might not be the whole story.
This is purely anecdotal experience, but with semi-auto weapons, every shot you fire feels
like it has more "weight" to it since you have to deliberately trigger each one.
This is actually something the developers at Valve noticed when developing Team Fortress 2:
they made most of the weapons in that game semi-automatic *on purpose* so that
players felt like they contributed more to the outcome of each gunfight
(I'm too tired tonight to find a good source, but if you really want me to, [bug me about it](https://plush.city/@kebokyo)).
I have a feeling that because I was putting more mental effort into each shot,
I also put more *physical* effort into each shot &mdash; increasing the amount of noise
each click produced and thus exacerbating the problem for my roommate.
There might have even been a chance that the specific pistol I used contributed to
the problem as well. I have been working to unlock cosmetics for the pistols carried forward
from the previous game, Modern Warfare II, so I have the P890 pistol on most of
my loadouts right now. The P890, like most guns in both games, is based on a real-life
weapon but given a fake name to avoid legal trouble. In this case, the P890 is based off of
the Sig Sauer P226, a heavy-duty pistol that emphasises the *heavy* part, both in caliber (.45 ACP)
and in weight (its frame is still made out of steel!). In-game, the pistol has
a high damage output but a slow rate-of-fire to compensate, which means I have to be
even *more* deliberate with my shots &mdash; further emphasizing the mental (and,
indrectly, physical) effort I put into my mouse clicks.
![A screenshot of the P890 from Call of Duty: Modern Warfare II. It is a blocky steel-framed pistol with an exposed hammer protruding from the back of the slide. Image originally from PCGamesN: https://www.pcgamesn.com/call-of-duty-modern-warfare-2/best-p890-loadout](/blog/pcgamesn_p890.jpg)
---
In the end, I made a personal rule that I will not play COD or other input-intensive games
like FPS games in the evening, where my hardcore gamering might disturb my roommates' other evening
activities. Of course, with how hot the weather has been getting lately, that effectively means that
I am *never* allowed to play these games (as a gaming PC running on full blast generates a ton of heat already)... but that may be more of a blessing than a curse if I am being honest with myself.
Oh, before I go, here's my very general opinion on MWIII: multiplayer is a blast, Modern Warfare Zombies
is an insult to both DMZ fans and COD Zombies fans, and I haven't touched the campaign yet because
I'm still trying to finish the prior game's campaign first. I have a *lot* more to say
(including how I feel about the Gunsmith weapon customization systems in both this game and
Black Ops 6)... but that is for another time.
Hope y'all enjoyed this post, as ranty as it is. Since this is more of a "long journal entry" than
"short blog post", here is the [obligatory song link](https://www.youtube.com/watch?v=HbwzOHjZUkg). Cya!

View file

@ -0,0 +1,8 @@
---
date: 2030-01-01 13:00:00
title: Comment Markup
summary: haha penis
---
import Comments from "../../components/Comments.astro"
Testity test test

10
src/env.d.ts vendored
View file

@ -1 +1,9 @@
/// <reference path="../.astro/types.d.ts" />
/// <reference path="../.astro/types.d.ts" />
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;
}
}

View file

@ -13,14 +13,13 @@ import Footer from '../components/Footer.astro'
import MDXImage from '../components/mdx/MDXImage.astro'
import HR from '../components/HR.astro'
import Comments from '../components/Comments.astro'
const fm = Astro.props.frontmatter
const headings = await Astro.props.headings
const title = fm.title ?? 'blog post'
const ogImageUrl = "https://eleboog.com/blog/blogbanner.png"
const numberToWord = (num: number) => {
switch(num) {
case 1: return "one";
@ -40,21 +39,6 @@ const numberToWord = (num: number) => {
<meta name="viewport" content="width=device-width" />
<meta name="generator" content={Astro.generator} />
<title>{title ? title + ' - eleboog.com' : 'eleboog.com'}</title>
{/* open graph babeeeee */}
<meta property="og:title" content={title ? title + ' - eleboog.com' : 'eleboog.com'} />
<meta property="og:site_name" content="eleboog.com" />
<meta property="og:description" content={fm.summary ?? "My personal website, containing blog posts, a mini journal, and other projects. The dark side of the sauce."} />
<meta property="og:type" content="article" />
<meta property="og:article:published_time" content={fm.date.replace(' ', 'T')} />
<meta property="og:article:modified_time" content={fm.updated ? fm.updated.replace(' ', 'T') : fm.date.replace(' ', 'T')} />
<meta property="og:image" content={ fm.cover ? 'https://eleboog.com' + fm.cover : ogImageUrl } />
<meta property="og:image:secure_url" content={ fm.cover ? 'https://eleboog.com' + fm.cover : ogImageUrl } />
<meta property="og:image:width" content="1280" />
<meta property="og:image:height" content="720" />
<meta property="twitter:title" content={title ? title + ' - eleboog.com' : 'eleboog.com'} />
<meta property="twitter:card" content="summary_large_image" />
<meta property="twitter:image" content={ fm.cover ? 'https://eleboog.com' + fm.cover : ogImageUrl } />
</head>
<body class="main-spacing">
<Header current='blog post'/>
@ -106,6 +90,7 @@ const numberToWord = (num: number) => {
<slot/>
</article>
</main>
<Comments/>
<Footer/>
</body>
</html>

View file

@ -8,13 +8,10 @@ import Header from '../components/Header.astro'
import Footer from '../components/Footer.astro'
import HR from '../components/HR.astro'
import { flatMap } from 'lodash'
const { frontmatter } = Astro.props
const title = frontmatter.slug ?? 'whoops'
const ogImageUrl = "https://eleboog.com/blog/blogbanner.png"
const title = frontmatter.slug ?? 'me'
---
@ -26,19 +23,6 @@ const ogImageUrl = "https://eleboog.com/blog/blogbanner.png"
<meta name="viewport" content="width=device-width" />
<meta name="generator" content={Astro.generator} />
<title>{title ? title + ' - eleboog.com' : 'eleboog.com'}</title>
{/* open graph babeeeee */}
<meta property="og:title" content={title ? title + ' - eleboog.com' : 'eleboog.com'} />
<meta property="og:site_name" content="eleboog.com" />
<meta property="og:description" content={frontmatter.summary ?? "My personal website, containing blog posts, a mini journal, and other projects. The dark side of the sauce."} />
<meta property="og:type" content="website" />
<meta property="og:image" content={ ogImageUrl } />
<meta property="og:image:secure_url" content={ ogImageUrl } />
<meta property="og:image:width" content="1280" />
<meta property="og:image:height" content="720" />
<meta property="twitter:title" content={title ? title + ' - eleboog.com' : 'eleboog.com'} />
<meta property="twitter:card" content="summary_large_image" />
<meta property="twitter:image" content={ ogImageUrl } />
</head>
<body class="main-spacing">
<Header current={title}/>

11
src/lib/auth-client.ts Normal file
View file

@ -0,0 +1,11 @@
import { createAuthClient } from "better-auth/react"
import { usernameClient, adminClient, passkeyClient } from "better-auth/client/plugins"
export const authClient = createAuthClient({
/** The base URL of the server (optional if you're using the same domain) */
baseURL: "http://localhost:4321",
plugins: [
usernameClient(), adminClient(), passkeyClient()
]
})
export const { signIn, signOut, useSession } = authClient;

30
src/lib/auth.ts Normal file
View file

@ -0,0 +1,30 @@
import 'dotenv/config'
import { betterAuth } from "better-auth";
import { username, admin } from "better-auth/plugins"
import { passkey } from "better-auth/plugins/passkey";
//import { Pool } from "pg";
import { prismaAdapter } from "better-auth/adapters/prisma";
import prisma from "./prisma"
export const auth = betterAuth({
plugins: [
username(), admin(),
passkey({
rpID: "localhost",
rpName: "eleboog.com"
})
],
socialProviders: {
discord: {
clientId: process.env.DISCORD_CLIENT_ID!,
clientSecret: process.env.DISCORD_CLIENT_SECRET!,
},
github: {
clientId: process.env.GITHUB_CLIENT_ID!,
clientSecret: process.env.GITHUB_CLIENT_SECRET!,
},
},
database: prismaAdapter(prisma, {
provider: "postgresql"
}),
})

7
src/lib/prisma.ts Normal file
View file

@ -0,0 +1,7 @@
import { PrismaClient } from "@prisma/client";
const prisma: PrismaClient = new PrismaClient({
datasourceUrl: import.meta.env.DATABASE_URL,
})
export default prisma;

19
src/middleware.ts Normal file
View file

@ -0,0 +1,19 @@
import { auth } from "./lib/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();
});

View file

@ -0,0 +1,65 @@
---
import BaseLayout from '../../layouts/BaseLayout.astro';
import HR from '../../components/HR.astro';
import Icon from '../../components/Icon.astro';
---
<style>
@import url('/src/styles/globals.css');
@tailwind components;
@layer components {
input[type="text"] {
@apply ml-2 px-2 text-current bg-crusta-200 dark:bg-night-900 dark:bg-opacity-80;
}
.passkey {
@apply p-2 border-2 rounded-md border-crusta-800 dark:border-night-200 bg-crusta-300 dark:bg-night-800 text-current hover:underline;
}
.sso {
@apply p-2 border-2 rounded-md border-gray-600 dark:border-gray-300 text-current hover:underline;
}
a {
@apply text-subtitle hover:underline;
}
}
</style>
<BaseLayout title="login">
<h2 class="font-serif text-3xl my-2">login to eleboog.com</h2>
<p>With an eleboog.com account, you can comment on blog posts and get access to exclusive content.</p>
<HR/>
<article class="space-y-2">
<div class="mb-2">
<button class="passkey">
login with passkey&ensp;<Icon icon="passkey" class="inline fill-current w-8 h-8" title="passkey" aira-label="passkey"/>
</button>
&ensp;
<span>( <a href="/help/passkeys" class="font-mono">wait, huh?</a> )</span>
</div>
<!--<a class="font-serif text-sm">register with passkey</a>-->
<p>&mdash; or &mdash;</p>
<p>login or register with an existing account below:</p>
<div class="space-x-4">
<button class="sso" id="discord"><Icon icon="FaDiscord" class="inline fill-current h-8" title="discord" aria-label="discord"/>&ensp;discord</button>
<button class="sso" id="github"><Icon icon="FaGitHub" class="inline fill-current h-8" title="github" aria-label="github"/>&ensp;github</button>
<!--<button class="sso"><Icon icon="FaGoogle" class="inline fill-current h-8" title="google" aria-label="google"/>&ensp;google</button>-->
</div>
</article>
</BaseLayout>
<script>
const { signIn, signOut } = await import("../../lib/auth-client");
const discordBtn = document.querySelector("#discord");
const githubBtn = document.querySelector("#github");
discordBtn?.addEventListener('click', () => signIn.social({
provider: "discord",
callbackURL: "/",
}))
githubBtn?.addEventListener('click', () => signIn.social({
provider: "github",
callbackURL: "/",
}))
</script>

View file

@ -0,0 +1,85 @@
---
export const prerender = false
import BaseLayout from '../../../layouts/BaseLayout.astro';
import HR from '../../../components/HR.astro';
import Icon from '../../../components/Icon.astro';
import prisma from '../../../lib/prisma';
import { actions } from 'astro:actions';
if (!Astro.locals.session || !Astro.locals.user) {
return Astro.redirect("/account/login");
}
const user = Astro.locals.user
if (Astro.request.method === "POST") {
try {
const input = await Astro.request.formData();
const username = input.get("username");
const result = await prisma.user.update({
where: {
id: user?.id,
},
data: {
username: username?.toString(),
},
});
return Astro.redirect(Astro.request.url);
/*const { data, error } = await Astro.callAction(actions.updateUsername, {
id: user.id,
username: username,
});*/
} catch (error) {
if (error instanceof Error) {
console.error(error.message);
}
}
}
---
<style>
@import url('/src/styles/globals.css');
@tailwind components;
@layer components {
input[type="text"] {
@apply ml-2 px-2 text-current bg-crusta-200 dark:bg-night-900 dark:bg-opacity-80;
}
.passkey {
@apply p-2 border-2 rounded-md border-crusta-800 dark:border-night-200 bg-crusta-300 dark:bg-night-800 text-current hover:underline;
}
.sso {
@apply p-2 border-2 rounded-md border-gray-600 dark:border-gray-300 text-current hover:underline;
}
a {
@apply text-subtitle hover:underline;
}
}
</style>
<BaseLayout title="account settings">
<h2 class="font-serif text-3xl my-2">account settings</h2>
<HR/>
<article class="space-y-2">
<h3 class="font-serif text-2xl">user info</h3>
<h4 class="font-mono text-2xl">username</h4>
{user.username ? <p>
Your current username is {user.username}. You can change your username below.
</p> : <p>
<span class="text-red-600 dark:text-red-400">Huh, you do not seem to have a username yet. You should fix that. Like, now.</span>
</p>}
<form method="POST" class="flex">
<label for="username">username:
<input type="text" name="username" id="username" required/>
</label>
&emsp;
<input type="submit" value="update" class="text-subtitle hover:underline"/>
</form>
</article>
</BaseLayout>

View file

@ -0,0 +1,6 @@
import { auth } from "../../../lib/auth"; // import your Better Auth instance
import type { APIRoute } from "astro";
export const ALL: APIRoute = async (ctx) => {
return auth.handler(ctx.request);
};

View file

@ -15,7 +15,7 @@ const archives = await getCollection('archives');
---
<BaseLayout title="archives" description="The main list of all of the blog posts and other articles I have written on this site, including some that were written in prior versions of the site.">
<BaseLayout title="archives">
<h1 class="font-serif text-3xl my-2">Blog Archive</h2>
<p>This is a list of all blog posts that have ever been posted on this site.</p>

View file

@ -15,7 +15,6 @@ I thought about making a blogroll, but I decided instead to make a button galler
<a href="https://www.5snb.club"><img width="88" height="31" src="/buttons/522@5snb.club.png" class="border-none shadow-none inline rounded-none" alt="A button for 522 at 5snb dot club."/></a>
<a href="https://foxscot.ch"><img width="88" height="31" src="/buttons/foxscot.ch.png" class="border-none shadow-none inline rounded-none" alt="A button for Foxscotch."/></a>
<a href="http://legacy.sominemo.com"><img width="88" height="31" src="/buttons/sominemo.gif" class="border-none shadow-none inline rounded-none" alt="A button for Sominemo."/></a>
<a href="https://bentley.trashcan.lol"><img width="88" height="31" src="/buttons/bentley_88x31.png" class="border-none shadow-none inline rounded-none" alt="A button for Bentley."/></a>
</div>
{ false && <>...and here's a list of friends I am publicly shaming into making buttons because I am a gremlin:

View file

@ -0,0 +1,95 @@
---
layout: '../../layouts/StandaloneMDXLayout.astro'
title: MDX Stylesheet
date: 1970-01-01
summary: "A showcase of everything I can do with MDX + the addons I've installed."
slug: mdxsheet
draft: true
---
Woah, how did you find this? You must be... a wizard or something. Or a witch. I like that idea better. You're a witch.
# Base Markdown (CommonMark)
OK, this should be fairly obvious.
Regular text is by default grouped into paragraphs. Each paragraph is separated by a double line break. In other words, there needs to be
a blank line inbetween each paragraph...
...like I just did! If you don't have a blank line inbetween line breaks, the line break will be ignored.
This is great if you're writing in a code editor (like I am) where there is no autowrap:
you can just break the line within the editor...
whenever you need to...
so you can keep all your text in view without having to scroll horizontaly.
```mdx
you can just break the line within the editor...
whenever you need to...
so you can keep all your text in view without having to scroll horizontaly.
```
Technical note: if you break the line without a space inbetween
the words, a space will be automatically added so you don't accidentally make a catdog word. If there *is* a space before the break, that
space will be respected.
If you *want* the line break to actually happen, there are multiple ways to do it.
The first method is to add **two or more** spaces after the line you want to break.`°°`
The second method is to add a backslash before the line break so the break is "escaped" and isn't cut out like it usually is.`\`\
Finally, you can just be silly and add an HTML break element.`<br/>`<br/>
<p class="text-subtitle text-sm mb-2">today i learned that • in Monofur is actually rendered as `•`. cute.</p>
Honestly, if I were to micromanage the MDX parser, i would *disable* the two-space trick and force myself to make line breaks more
visually obvious. I hate the two-space thing. With a passion.\
My favorite is probably the escape method followed by the HTML method.
You aren't limited to just `<br/>` for HTML: *any HTML elements can be inserted into your Markdown document,* and it will be passed
through just fine.
```html
<div class="border-red-500 border-2 rounded-xl border-dashed p-4 mb-2">
<p class="mb-0 text-green-700 dark:text-green-400">hiiiii</p>
</div>
```
<div class="border-red-500 border-2 rounded-xl border-dashed p-4 mb-2">
<p class="mb-0 text-green-700 dark:text-green-400">hiiiii</p>
</div>
The classes here are [Tailwind](https://tailwindcss.com) classes so I can style individual elements however I want without having to write
50 billion lines of CSS.
You may notice that I put `mb-2` for the `div` but `mb-0` for the `p`. What's up with that?\
`p` is a default element in Markdown &mdash; every paragraph I write in Markdown is wrapped with a `p` element. Thus, I added my own CSS styling
to `p` to make everything look nice. That includes a `mb-2` by default. This styling happens *after* all the Markdown is parsed into HTML,
which means the explicit `p` is *also* affected.
So, to make sure there isn't extra white space within the HTML `div`, I reset the bottom margin of the `p` to `mb-0`.
The CSS styling itself is done through a `markdown.css` file that automagically applies certain Tailwind classes to any elements inside an
`article` tag, a.k.a. the entirety of the parsed Markdown.
```css
article {
//...
p {
@apply text-black dark:text-night-100 mb-2
}
//...
}
```
<p class="text-subtitle text-sm mb-2">looks like I need to add styling for CSS code lmao</p>
Fun fact: this `dark:` tag is basically how I style the entirety of the site for dark mode. It's also why, currently, dark mode is based
on your system or browser's dark mode setting: it Just Works™ and I don't want to make it more complicated right now.
---
Headers work like you would expect.
# Header 1
## Header 2
### Header 3
#### Header 4
##### Header 5 - oh, i didnt make styling for this one oops
###### Header 6 - not this one either. i'll get around to it eventually

180
src/pages/help/passkeys.mdx Normal file
View file

@ -0,0 +1,180 @@
---
title: "Wait, what's this \"login with passkey\" button?"
date: 1970-01-01
summary: A brief guide on how to setup passkeys for your eleboog.com account.
layout: '../../layouts/StandaloneMDXLayout.astro'
slug: passkeys
draft: true
---
import Icon from '../../components/Icon.astro'
import MDXAccordion from '../../components/mdx/MDXAccordion.astro'
You might have noticed when trying to [login or register](/account/login) for an eleboog.com account, there is no option to input an email and
password like most other sites, especially small ones like mine. Instead, there is this button:
<button class="passkey mb-2 p-2 border-2 rounded-md border-crusta-800 dark:border-night-200 bg-crusta-300 dark:bg-night-800 text-current hover:underline; hover:cursor-default">
<span>login with passkey&ensp;<Icon icon="passkey" class="inline fill-current w-8 h-8" title="passkey" aira-label="passkey"/></span>
</button>
What the hell is a passkey? How do I log in with it? How do I *register* with it? Why should I use this over the Google link below it?
All of these questions and more will be answered in this tome, so strap in. We're about to open some windows.
If you just want to figure out how to *use* passkeys and don't care about how it works and its limitations, check [The Table&reg;](#the-table) to
see if your OS and browser combination like passkeys, and if they do, follow the tutorials on [adding a passkey to your account](#registering-a-new-passkey-on-an-existing-account)
and [logging in with it](#logging-in-with-your-passkey).
# what even *is* a passkey?
The best way to describe a passkey is a secret handshake between you and a website or service. Instead of having to remember a password
(and making sure you don't accidentally give anyone your password, either on your own or through a security breach), your computer stores
the credentials you need, verifies it's you before handing it out (usually with biometric authentication such as a fingerprint or FaceID), and
then the server checks your computer's credentials with the info it has on you to make sure you are who you say you are.
If you want to understand the gobblygook I just threw at you, expand this dropdown for a crash course.
<MDXAccordion title="nerd shit">
To distill all of that into layman's terms, let's say I want to give you a way to confirm who I am when I meet up with you, no matter where we are
or what we are doing. A great way to do this would be to make a very special key, a key that I keep close and protect at all times. I then make
a *shitton* of locks with this special key and give them out to all of the people I want to meet up with, including you. You now hold a special lock
that can only be opened by my special key. When we meet up, you show me your lock, and I try to use my key to open the lock. If it opens, then
I've confirmed that I am, in fact, me! If not, then the key that I am holding is likely a counterfit, and the person you are talking to right
now is *not* me.
This is the basics of private/publickey authentication: you generate a pair of keys, one that's kept to yourself (private) and one that you give out
to all of the people or services that you want to share your identity with (public). To authenticate yourself, the server gives you the public
key you gave them, and you match it with your private key. If they pair up successfully, congrats, you're in! If not, then you're rejected.
Of course, you don't actually give the server your private key: that would be *stupidly insecure*. Instead, the server gives you a randomized
string (a "challenge") to play with. You encrypt the string with your private key and give the string back to the server. The server then
attempts to *de*crypt your string with the public key you gave it. If everything matches up, it should be able to decrypt the string *back*
to its original form. Private key encrypts, public key decrypts, everybody wins.
Still, this leads one important question: what if your private key gets stolen? That will mean all anyone has to do is find the people who
are holding your locks (public keys), and all of a sudden your identity has been stolen!!! Oh no!!!11!!1! However, we can prevent this by
keeping the private key in its *own* locked box, only able to be opened by *another* security check. In computer-land, this means we *encrypt*
the private key, and require another form of authentication to properly decrypt it. Back in the day, this was, whoops, another password (or
"passphrase"), but for most passkey implementations, this second layer of authentication is usually some sort of biometric authenticaion.
So, on your iPhone for example, when you use a passkey on my site, the following (very simplified) steps happen:
1. The server gives a randomly generated string (a "challenge") to your browser as part of a request for your passkey.
2. The browser passes that request to iOS, which gives you a prompt to confirm whether you want to log in with your stored passkey.
3. Once you hit the okay button, iOS performs FaceID to authenticate you. If that succeeds, iOS is able to decrypt your private key.
4. iOS then encrypts the challenge string with your **private** key and gives it to the browser along with the ID's of your user account and your passkey itself.
5. The browser sends this information to the server.
6. The server then finds the **public** key associated with your account's passkey and attempts to *de*crypt the challenge with it. If the challenge
successfully decrypts back into its original form, then it knows your private key matched its public key and lets you in!
</MDXAccordion>
## ok, why should I care?
Passwords suck. They suck massive booty cheeks.
1. You need to remember the password in order to log in. You're human. You forget things.
2. So you write it down somewhere. Sometimes on sticky notes. Sometimes in password books. Smart people store them in password managers like
KeePass or 1Password.
3. But wherever you store the password, it is fucked. Write it on a sticky note? Whoever walks by your computer can now log in.
Write it in a book? Someone finds and/or steals the book, now they know your password. Put it in a password manager? [The password manager
gets breached and now all of your precious passwords belong to the hacker known as 4chan.](https://www.tomsguide.com/computing/password-managers/millions-stolen-from-lastpass-users-in-massive-hack-attack-what-you-need-to-know)
4. The only way you can prevent your password from being stolen is to just... not write it down. Which means you have to remember it. Oh. We're back at the start again.
This isn't even getting into how the services you make passwords for can screw them up (like storing your passwords in plain text for anyone to see
that logs into the server yaaayyyyy).
One solution to this is to use another, big account you already have to log into all of your new, smaller ones &mdash; this is known as Single Sign-On, or SSO for short.
Of course, this means that the big account is *especially* important to secure as getting access to that account gives an attacker access to *all* of your accounts (tho since most people use the same email address for everything, this is already kind of a given).
Another solution to this is to require another method to authenticate yourself after you put in your password. This is known as Two-Factor Authentication, or 2FA for short.
This is great as a way to make an existing system more secure, but many 2FA methods have their own problems (if you use emails, your email account could get hacked; if you use your phone, your SIM card could get hacked; if you use apps like Google Authenticator, the account holding the codes could get hacked, or if the codes are just held on your phone, your phone could get stolen or lost; etc etc etc etc etc).
In my opinion, passkeys are the best existing solution to this problem. 2FA is often built-in (in the form of biometric authentication required before your passkey can be used), it doesn't have to rely on
another account or service to function properly, and *working* implementations are usually very smooth to operate, both in logging in with and registering a new passkey.
...of course, that leads to the biggest problem with passkeys...
# passkeys don't work for everyone
For some incredibly fucked up reason I cannot fathom why it hasn't been fixed already other than everyone being too individualistic to realize the
potential of passkeys being a global shared system that Just Works&trade;... Passkeys don't, in fact, Just Work&trade;. Whether you will be able
to use passkeys depends on three factors: your **operating system**, your **browser**, and whether you're using **third-party password managers**
that have their own passkey support (and want to use that support to keep everything in one place).
## The Table&reg;
The following table is (my best) catalogue of what combination of browsers and OS's currently work with passkeys, along with how easy it is to
let third-party passkey managers work with said combination. Disclaimer: this is all in my personal experience, so there are gaps in my knowledge.
<table>
<tr>
<th>&darr;OS's<br/>&rarr;Browsers</th>
<th>Chrome</th>
<th>Firefox</th>
<th>Safari</th>
<th>Other</th>
</tr>
<tr>
<td>Windows</td>
<td>not sure</td>
<td>No, requires TP extension</td>
<td>n/a</td>
<td>Mileage May Vary&trade;</td>
</tr>
<tr>
<td>macOS</td>
<td>No, requires TP extension</td>
<td>No, requires TP extension</td>
<td>Yes + TP (extensions)</td>
<td>Mileage May Vary&trade;</td>
</tr>
<tr>
<td>iOS / iPadOS</td>
<td>n/a</td>
<td>n/a</td>
<td>Yes + TP (native)</td>
<td>n/a</td>
</tr>
<tr>
<td>Android</td>
<td>Yes + TP (native)</td>
<td>not sure</td>
<td>n/a</td>
<td>Mileage May Vary&trade;</td>
</tr>
<tr>
<td>Linux</td>
<td>not sure yet</td>
<td>not sure yet</td>
<td>n/a</td>
<td>Mileage May Vary&trade;</td>
</tr>
</table>
<MDXAccordion title="notes on passkey support">
- Typically, the support offered on other browsers depends on what browser they're based on. Chromium browsers will act like Chrome, Firefox
forks will act like Firefox, every browser on iOS (except in the EU) will act like Safari, etc. Still, since these browsers run on their own
codebases forked off from the mainline codebase, updates mainstream browsers get to improve passkey support may not come to forks in a timely
manner, if *ever* (Pale Moon, I am giving you criminal offensive side-eye).
- Firefox seems to be allergic to passkey support. On both Windows and macOS, passkey requests just result in a "Insert authentication key"
popup. The only way to fix this is to add an extension for a third-party passkey manager, in which case the extension will see the request and
handle it on its own.
- Chrome also seems to have this problem on macOS. I assume Apple isn't very concerned with making their system-level passkey manager work with
third-party browsers.
- On iOS, passkeys seem to be handled mostly on an OS-level basis. Since every web browser on iOS is Safari in a trenchcoat outside of the EU,
it's possible to make all browsers, even third-party ones, just let Jesus (Tim Cook) take the wheel. This is great for ensuring passkeys work
system-wide... but bad for other, unrelated reasons. I'm not sure if things change in the EU, since browsers are allowed to have alternative
web engines over there (instead of being forced to use Safari's WebKit).
- I have no fucking idea what the state of passkey support is on Linux. I am switching over to it very soon, so hopefully I find out soon too.
I assume there are differences between major distributions, so once I start exploring Linux, I will have categores for Ubuntu, Debian,
Fedora, Arch, and "Other Linux".
</MDXAccordion>
# registering a new passkey on an existing account
To register a passkey on your existing account, first, [go to your account security settings](/account/settings/security).
# logging in with your passkey
# registering a new account with a passkey

View file

@ -3,7 +3,6 @@ layout: '../layouts/StandaloneMDXLayout.astro'
title: Me!
date: 1970-01-01
summary: "A page for stuff about me and what I'm doing."
slug: me
draft: true
---
@ -41,11 +40,6 @@ There used to be a link to my Cohost (`@kebokyo`), but Cohost is unfortunately s
If you like what I do and want to help me make more of it, currently the only way I have for you to give me money is through [Ko-fi](https://ko-fi.com/kebokyo). I am a very very broke college student and any little bit of help means a lot to me!
<MDXCallout>
Most of the information below is out of date since I have just graduated college and thus had a massive shift in my day-to-day operations.
Expect updates soon(ish).
</MDXCallout>
# why
Why do I even have this website? Well, it's for mulitple reasons:
@ -76,7 +70,7 @@ I also carry my [Nintendo 3DS](https://en.wikipedia.org/wiki/Nintendo_3DS) almos
# colophon
This website runs on a [~~Hetzner VPS~~](https://www.hetzner.com) [~~Vultr VPS~~](https://www.vultr.com/) actually I'm going back to Hetzner now lmao, Nginx web server, and [~~Next.js metaframework~~](https://nextjs.org) [Astro web framework](https://astro.build).
This website runs on a [Hetzner VPS](https://www.hetzner.com), Nginx web server, and [~~Next.js metaframework~~](https://nextjs.org) [Astro web framework](https://astro.build).
The reason this site looks so fancy in both light & dark mode is because of [Tailwind CSS](https://tailwindcss.com) and its built-in light & dark mode features. I may build a special toggle so you can choose which version of the site you like more one day, but for now, it's just based on whether your browser / OS is in light or dark mode.
@ -93,13 +87,7 @@ When I insert code into my blog, I use [React Code Block](https://react-code-blo
I use the <a href="https://www.npmjs.com/package/feed" class="text-subtitle text-base hover:underline px-1 bg-neutral-300 bg-opacity-50 dark:bg-slate-600 dark:bg-opacity-100 rounded-md font-mono">feed</a> library to generate my Atom & JSON feeds ~~for everything except narrated articles. For that, I use <a href="https://www.npmjs.com/package/feed" class="text-subtitle text-base hover:underline px-1 bg-neutral-300 bg-opacity-50 dark:bg-slate-600 dark:bg-opacity-100 rounded-md font-mono">podcast-rss</a> since it has more features specifically to comply with platforms' weird-ass rules
regarding podcast feeds.~~ Right now, narrated articles have been scrapped, but when I do start making audio content, I'll use `podcast-rss` to generate a feed specifically for that audio content.
Finally, for the software I use to actually write the code...
I ~~used to~~ *currently* use [Visual Studio Code](https://code.visualstudio.com)~~, but since it's an Electron app
(basically Google Chrome in a fancy native-app-shaped box), its performance and resource use is very high.
Thus, I switched *back* to [Sublime Text](https://www.sublimetext.com).
If anyone has a FOSS alternative to Sublime that works just as well, let me know cause that would be awesome.~~
as eventually I just realized the bomb is too powerful and the good it does on my workflow outweighs the bad it does on my computer.
Weeeeeeeeeeeeeeeeeee!!!
Finally, for the software I use to actually write the code... I used to use [Visual Studio Code](https://code.visualstudio.com), but since it's an Electron app (basically Google Chrome in a fancy native-app-shaped box), its performance and resource use is very high. Thus, I switched *back* to [Sublime Text](https://www.sublimetext.com). If anyone has a FOSS alternative to Sublime that works just as well, let me know cause that would be awesome.
# ideas
@ -123,13 +111,10 @@ Qdoba is better. Fight me. (plus I'm poor so I don't get either of them very oft
and for today since I got free double protien I double-wrapped it just in case.
If you take me to chipotle and get this exact order (minus the protien because i think it's seasonal lol), I will make out with you.
If you take me to chipoltle and get this exact order (minus the protien because i think it's seasonal lol), I will make out with you.
# ai
I do not use generative AI on this website. Period.
The only time I used generative AI for any real purpose was during a massive crunch session in college, and even then I was disappointed in it.
I need to write a post about it one day.
One day, I need to add a `robots.txt` and other such protections to prevent this website from being scraped... but no-one is going to listen to me anyway because my rights don't matter to them in that regard.

View file

@ -1,23 +0,0 @@
---
date: 1970-01-01
layout: '../layouts/StandaloneMDXLayout.astro'
title: More Pages
slug: more
summary: Other miscleaneous pages on my website.
---
Want to see even more stupid stuff that I've been up to? Here you go!
- [sharefeed - A collection of links to things that are interesting](/sharefeed)
- [me - An "about me" page and a whole lot more](/me)\
If you are a fan of slash pages, try throwing some into the URL and see what happens!
For example, [/colophon](/colophon) or [/chipotle](/chipotle) :3
- [cool - A miscellaneous resource of cool stuff I want to draw more attention to](/cool)
- [rss - Want to keep tabs on what I make? This is the best way to do it](/rss)\
If you need an RSS client and are on iOS and/or macOS, check out [NetNewsWire](https://netnewswire.com/) (not sponsored, it's just what
I use lol)
If you have suggestions for what else to put on my website, please let me know ^w^

View file

@ -82,4 +82,16 @@ article {
img {
@apply relative -z-10 border-4 border-crusta-200 dark:border-night-800 rounded-lg shadow-lgr shadow-crusta-400/20 dark:shadow-night-400/50 my-2
}
table {
@apply border-2 w-full border-collapse mb-2
}
tr {
@apply border-2
}
th {
@apply border-2
}
td {
@apply border-2 text-center
}
}

1
src/svg/FaDiscord.svg Normal file
View file

@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 640 512"><!--!Font Awesome Free 6.7.2 by @fontawesome - https://fontawesome.com License - https://fontawesome.com/license/free Copyright 2025 Fonticons, Inc.--><path d="M524.5 69.8a1.5 1.5 0 0 0 -.8-.7A485.1 485.1 0 0 0 404.1 32a1.8 1.8 0 0 0 -1.9 .9 337.5 337.5 0 0 0 -14.9 30.6 447.8 447.8 0 0 0 -134.4 0 309.5 309.5 0 0 0 -15.1-30.6 1.9 1.9 0 0 0 -1.9-.9A483.7 483.7 0 0 0 116.1 69.1a1.7 1.7 0 0 0 -.8 .7C39.1 183.7 18.2 294.7 28.4 404.4a2 2 0 0 0 .8 1.4A487.7 487.7 0 0 0 176 479.9a1.9 1.9 0 0 0 2.1-.7A348.2 348.2 0 0 0 208.1 430.4a1.9 1.9 0 0 0 -1-2.6 321.2 321.2 0 0 1 -45.9-21.9 1.9 1.9 0 0 1 -.2-3.1c3.1-2.3 6.2-4.7 9.1-7.1a1.8 1.8 0 0 1 1.9-.3c96.2 43.9 200.4 43.9 295.5 0a1.8 1.8 0 0 1 1.9 .2c2.9 2.4 6 4.9 9.1 7.2a1.9 1.9 0 0 1 -.2 3.1 301.4 301.4 0 0 1 -45.9 21.8 1.9 1.9 0 0 0 -1 2.6 391.1 391.1 0 0 0 30 48.8 1.9 1.9 0 0 0 2.1 .7A486 486 0 0 0 610.7 405.7a1.9 1.9 0 0 0 .8-1.4C623.7 277.6 590.9 167.5 524.5 69.8zM222.5 337.6c-29 0-52.8-26.6-52.8-59.2S193.1 219.1 222.5 219.1c29.7 0 53.3 26.8 52.8 59.2C275.3 311 251.9 337.6 222.5 337.6zm195.4 0c-29 0-52.8-26.6-52.8-59.2S388.4 219.1 417.9 219.1c29.7 0 53.3 26.8 52.8 59.2C470.7 311 447.5 337.6 417.9 337.6z"/></svg>

After

Width:  |  Height:  |  Size: 1.2 KiB

1
src/svg/FaGitHub.svg Normal file
View file

@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 496 512"><!--!Font Awesome Free 6.7.2 by @fontawesome - https://fontawesome.com License - https://fontawesome.com/license/free Copyright 2025 Fonticons, Inc.--><path d="M165.9 397.4c0 2-2.3 3.6-5.2 3.6-3.3 .3-5.6-1.3-5.6-3.6 0-2 2.3-3.6 5.2-3.6 3-.3 5.6 1.3 5.6 3.6zm-31.1-4.5c-.7 2 1.3 4.3 4.3 4.9 2.6 1 5.6 0 6.2-2s-1.3-4.3-4.3-5.2c-2.6-.7-5.5 .3-6.2 2.3zm44.2-1.7c-2.9 .7-4.9 2.6-4.6 4.9 .3 2 2.9 3.3 5.9 2.6 2.9-.7 4.9-2.6 4.6-4.6-.3-1.9-3-3.2-5.9-2.9zM244.8 8C106.1 8 0 113.3 0 252c0 110.9 69.8 205.8 169.5 239.2 12.8 2.3 17.3-5.6 17.3-12.1 0-6.2-.3-40.4-.3-61.4 0 0-70 15-84.7-29.8 0 0-11.4-29.1-27.8-36.6 0 0-22.9-15.7 1.6-15.4 0 0 24.9 2 38.6 25.8 21.9 38.6 58.6 27.5 72.9 20.9 2.3-16 8.8-27.1 16-33.7-55.9-6.2-112.3-14.3-112.3-110.5 0-27.5 7.6-41.3 23.6-58.9-2.6-6.5-11.1-33.3 2.6-67.9 20.9-6.5 69 27 69 27 20-5.6 41.5-8.5 62.8-8.5s42.8 2.9 62.8 8.5c0 0 48.1-33.6 69-27 13.7 34.7 5.2 61.4 2.6 67.9 16 17.7 25.8 31.5 25.8 58.9 0 96.5-58.9 104.2-114.8 110.5 9.2 7.9 17 22.9 17 46.4 0 33.7-.3 75.4-.3 83.6 0 6.5 4.6 14.4 17.3 12.1C428.2 457.8 496 362.9 496 252 496 113.3 383.5 8 244.8 8zM97.2 352.9c-1.3 1-1 3.3 .7 5.2 1.6 1.6 3.9 2.3 5.2 1 1.3-1 1-3.3-.7-5.2-1.6-1.6-3.9-2.3-5.2-1zm-10.8-8.1c-.7 1.3 .3 2.9 2.3 3.9 1.6 1 3.6 .7 4.3-.7 .7-1.3-.3-2.9-2.3-3.9-2-.6-3.6-.3-4.3 .7zm32.4 35.6c-1.6 1.3-1 4.3 1.3 6.2 2.3 2.3 5.2 2.6 6.5 1 1.3-1.3 .7-4.3-1.3-6.2-2.2-2.3-5.2-2.6-6.5-1zm-11.4-14.7c-1.6 1-1.6 3.6 0 5.9 1.6 2.3 4.3 3.3 5.6 2.3 1.6-1.3 1.6-3.9 0-6.2-1.4-2.3-4-3.3-5.6-2z"/></svg>

After

Width:  |  Height:  |  Size: 1.5 KiB

1
src/svg/FaGoogle.svg Normal file
View file

@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 488 512"><!--!Font Awesome Free 6.7.2 by @fontawesome - https://fontawesome.com License - https://fontawesome.com/license/free Copyright 2025 Fonticons, Inc.--><path d="M488 261.8C488 403.3 391.1 504 248 504 110.8 504 0 393.2 0 256S110.8 8 248 8c66.8 0 123 24.5 166.3 64.9l-67.5 64.9C258.5 52.6 94.3 116.6 94.3 256c0 86.5 69.1 156.6 153.7 156.6 98.2 0 135-70.4 140.8-106.9H248v-85.3h236.1c2.3 12.7 3.9 24.9 3.9 41.4z"/></svg>

After

Width:  |  Height:  |  Size: 478 B

View file

Before

Width:  |  Height:  |  Size: 1.3 KiB

After

Width:  |  Height:  |  Size: 1.3 KiB