mirror of
https://github.com/nestriness/nestri.git
synced 2025-12-12 08:45:38 +02:00
⭐feat: Add Steam account linking with team creation (#274)
## Description <!-- Briefly describe the purpose and scope of your changes --> <!-- This is an auto-generated comment: release notes by coderabbit.ai --> ## Summary by CodeRabbit - **New Features** - Introduced a real-time Steam login flow using QR codes and server-sent events (SSE) for team creation and authentication. - Added Steam account and friend management, including secure credential storage and friend list synchronization. - Integrated Steam login endpoints into the API, enabling QR code-based login and automated team setup. - **Improvements** - Enhanced data security by implementing encrypted storage for sensitive tokens. - Updated database schema to support Steam accounts, teams, memberships, and social connections. - Refined type definitions and consolidated account-related information for improved consistency. - **Bug Fixes** - Fixed trade ban status representation for Steam accounts. - **Chores** - Removed legacy C# Steam authentication service and related configuration files. - Updated and cleaned up package dependencies and development tooling. - Streamlined type declaration files and resource definitions. - **Style** - Redesigned the team creation page UI with a modern, animated QR code login interface. - **Documentation** - Updated OpenAPI documentation for new Steam login endpoints. <!-- end of auto-generated comment: release notes by coderabbit.ai -->
This commit is contained in:
94
packages/core/migrations/0010_certain_dust.sql
Normal file
94
packages/core/migrations/0010_certain_dust.sql
Normal file
@@ -0,0 +1,94 @@
|
||||
CREATE TYPE "public"."member_role" AS ENUM('child', 'adult');--> statement-breakpoint
|
||||
CREATE TYPE "public"."steam_status" AS ENUM('online', 'offline', 'dnd', 'playing');--> statement-breakpoint
|
||||
CREATE TABLE "steam_account_credentials" (
|
||||
"time_created" timestamp with time zone DEFAULT now() NOT NULL,
|
||||
"time_updated" timestamp with time zone DEFAULT now() NOT NULL,
|
||||
"time_deleted" timestamp with time zone,
|
||||
"steam_id" varchar(255) PRIMARY KEY NOT NULL,
|
||||
"refresh_token" text NOT NULL,
|
||||
"expiry" timestamp with time zone NOT NULL,
|
||||
"username" varchar(255) NOT NULL
|
||||
);
|
||||
--> statement-breakpoint
|
||||
CREATE TABLE "friends_list" (
|
||||
"time_created" timestamp with time zone DEFAULT now() NOT NULL,
|
||||
"time_updated" timestamp with time zone DEFAULT now() NOT NULL,
|
||||
"time_deleted" timestamp with time zone,
|
||||
"steam_id" varchar(255) NOT NULL,
|
||||
"friend_steam_id" varchar(255) NOT NULL,
|
||||
CONSTRAINT "friends_list_steam_id_friend_steam_id_pk" PRIMARY KEY("steam_id","friend_steam_id")
|
||||
);
|
||||
--> statement-breakpoint
|
||||
CREATE TABLE "members" (
|
||||
"id" char(30) NOT NULL,
|
||||
"team_id" char(30) NOT NULL,
|
||||
"time_created" timestamp with time zone DEFAULT now() NOT NULL,
|
||||
"time_updated" timestamp with time zone DEFAULT now() NOT NULL,
|
||||
"time_deleted" timestamp with time zone,
|
||||
"user_id" char(30),
|
||||
"steam_id" varchar(255) NOT NULL,
|
||||
"role" "member_role" NOT NULL,
|
||||
CONSTRAINT "members_id_team_id_pk" PRIMARY KEY("id","team_id")
|
||||
);
|
||||
--> statement-breakpoint
|
||||
CREATE TABLE "steam_accounts" (
|
||||
"time_created" timestamp with time zone DEFAULT now() NOT NULL,
|
||||
"time_updated" timestamp with time zone DEFAULT now() NOT NULL,
|
||||
"time_deleted" timestamp with time zone,
|
||||
"steam_id" varchar(255) PRIMARY KEY NOT NULL,
|
||||
"user_id" char(30),
|
||||
"status" "steam_status" NOT NULL,
|
||||
"last_synced_at" timestamp with time zone NOT NULL,
|
||||
"real_name" varchar(255),
|
||||
"member_since" timestamp with time zone NOT NULL,
|
||||
"name" varchar(255) NOT NULL,
|
||||
"profile_url" varchar(255),
|
||||
"username" varchar(255) NOT NULL,
|
||||
"avatar_hash" varchar(255) NOT NULL,
|
||||
"limitations" json NOT NULL,
|
||||
CONSTRAINT "idx_steam_username" UNIQUE("username")
|
||||
);
|
||||
--> statement-breakpoint
|
||||
CREATE TABLE "teams" (
|
||||
"id" char(30) PRIMARY KEY NOT NULL,
|
||||
"time_created" timestamp with time zone DEFAULT now() NOT NULL,
|
||||
"time_updated" timestamp with time zone DEFAULT now() NOT NULL,
|
||||
"time_deleted" timestamp with time zone,
|
||||
"name" varchar(255) NOT NULL,
|
||||
"owner_id" char(30) NOT NULL,
|
||||
"invite_code" varchar(10) NOT NULL,
|
||||
"slug" varchar(255) NOT NULL,
|
||||
"max_members" bigint NOT NULL,
|
||||
CONSTRAINT "idx_team_invite_code" UNIQUE("invite_code")
|
||||
);
|
||||
--> statement-breakpoint
|
||||
CREATE TABLE "users" (
|
||||
"id" char(30) PRIMARY KEY NOT NULL,
|
||||
"time_created" timestamp with time zone DEFAULT now() NOT NULL,
|
||||
"time_updated" timestamp with time zone DEFAULT now() NOT NULL,
|
||||
"time_deleted" timestamp with time zone,
|
||||
"email" varchar(255) NOT NULL,
|
||||
"avatar_url" text,
|
||||
"last_login" timestamp with time zone NOT NULL,
|
||||
"name" varchar(255) NOT NULL,
|
||||
"polar_customer_id" varchar(255),
|
||||
CONSTRAINT "idx_user_email" UNIQUE("email")
|
||||
);
|
||||
--> statement-breakpoint
|
||||
DROP TABLE "machine" CASCADE;--> statement-breakpoint
|
||||
DROP TABLE "member" CASCADE;--> statement-breakpoint
|
||||
DROP TABLE "steam" CASCADE;--> statement-breakpoint
|
||||
DROP TABLE "subscription" CASCADE;--> statement-breakpoint
|
||||
DROP TABLE "team" CASCADE;--> statement-breakpoint
|
||||
DROP TABLE "user" CASCADE;--> statement-breakpoint
|
||||
ALTER TABLE "steam_account_credentials" ADD CONSTRAINT "steam_account_credentials_steam_id_steam_accounts_steam_id_fk" FOREIGN KEY ("steam_id") REFERENCES "public"."steam_accounts"("steam_id") ON DELETE cascade ON UPDATE no action;--> statement-breakpoint
|
||||
ALTER TABLE "friends_list" ADD CONSTRAINT "friends_list_steam_id_steam_accounts_steam_id_fk" FOREIGN KEY ("steam_id") REFERENCES "public"."steam_accounts"("steam_id") ON DELETE cascade ON UPDATE no action;--> statement-breakpoint
|
||||
ALTER TABLE "friends_list" ADD CONSTRAINT "friends_list_friend_steam_id_steam_accounts_steam_id_fk" FOREIGN KEY ("friend_steam_id") REFERENCES "public"."steam_accounts"("steam_id") ON DELETE cascade ON UPDATE no action;--> statement-breakpoint
|
||||
ALTER TABLE "members" ADD CONSTRAINT "members_user_id_users_id_fk" FOREIGN KEY ("user_id") REFERENCES "public"."users"("id") ON DELETE cascade ON UPDATE no action;--> statement-breakpoint
|
||||
ALTER TABLE "members" ADD CONSTRAINT "members_steam_id_steam_accounts_steam_id_fk" FOREIGN KEY ("steam_id") REFERENCES "public"."steam_accounts"("steam_id") ON DELETE cascade ON UPDATE restrict;--> statement-breakpoint
|
||||
ALTER TABLE "steam_accounts" ADD CONSTRAINT "steam_accounts_user_id_users_id_fk" FOREIGN KEY ("user_id") REFERENCES "public"."users"("id") ON DELETE cascade ON UPDATE no action;--> statement-breakpoint
|
||||
ALTER TABLE "teams" ADD CONSTRAINT "teams_owner_id_users_id_fk" FOREIGN KEY ("owner_id") REFERENCES "public"."users"("id") ON DELETE cascade ON UPDATE no action;--> statement-breakpoint
|
||||
ALTER TABLE "teams" ADD CONSTRAINT "teams_slug_steam_accounts_username_fk" FOREIGN KEY ("slug") REFERENCES "public"."steam_accounts"("username") ON DELETE cascade ON UPDATE no action;--> statement-breakpoint
|
||||
CREATE UNIQUE INDEX "idx_member_steam_id" ON "members" USING btree ("team_id","steam_id");--> statement-breakpoint
|
||||
CREATE UNIQUE INDEX "idx_member_user_id" ON "members" USING btree ("team_id","user_id") WHERE "members"."user_id" is not null;--> statement-breakpoint
|
||||
CREATE UNIQUE INDEX "idx_team_slug" ON "teams" USING btree ("slug");
|
||||
651
packages/core/migrations/meta/0010_snapshot.json
Normal file
651
packages/core/migrations/meta/0010_snapshot.json
Normal file
@@ -0,0 +1,651 @@
|
||||
{
|
||||
"id": "56a4d60a-c062-47e5-a97e-625443411ad8",
|
||||
"prevId": "1717c769-cee0-4242-bcbb-9538c80d985c",
|
||||
"version": "7",
|
||||
"dialect": "postgresql",
|
||||
"tables": {
|
||||
"public.steam_account_credentials": {
|
||||
"name": "steam_account_credentials",
|
||||
"schema": "",
|
||||
"columns": {
|
||||
"time_created": {
|
||||
"name": "time_created",
|
||||
"type": "timestamp with time zone",
|
||||
"primaryKey": false,
|
||||
"notNull": true,
|
||||
"default": "now()"
|
||||
},
|
||||
"time_updated": {
|
||||
"name": "time_updated",
|
||||
"type": "timestamp with time zone",
|
||||
"primaryKey": false,
|
||||
"notNull": true,
|
||||
"default": "now()"
|
||||
},
|
||||
"time_deleted": {
|
||||
"name": "time_deleted",
|
||||
"type": "timestamp with time zone",
|
||||
"primaryKey": false,
|
||||
"notNull": false
|
||||
},
|
||||
"steam_id": {
|
||||
"name": "steam_id",
|
||||
"type": "varchar(255)",
|
||||
"primaryKey": true,
|
||||
"notNull": true
|
||||
},
|
||||
"refresh_token": {
|
||||
"name": "refresh_token",
|
||||
"type": "text",
|
||||
"primaryKey": false,
|
||||
"notNull": true
|
||||
},
|
||||
"expiry": {
|
||||
"name": "expiry",
|
||||
"type": "timestamp with time zone",
|
||||
"primaryKey": false,
|
||||
"notNull": true
|
||||
},
|
||||
"username": {
|
||||
"name": "username",
|
||||
"type": "varchar(255)",
|
||||
"primaryKey": false,
|
||||
"notNull": true
|
||||
}
|
||||
},
|
||||
"indexes": {},
|
||||
"foreignKeys": {
|
||||
"steam_account_credentials_steam_id_steam_accounts_steam_id_fk": {
|
||||
"name": "steam_account_credentials_steam_id_steam_accounts_steam_id_fk",
|
||||
"tableFrom": "steam_account_credentials",
|
||||
"tableTo": "steam_accounts",
|
||||
"columnsFrom": [
|
||||
"steam_id"
|
||||
],
|
||||
"columnsTo": [
|
||||
"steam_id"
|
||||
],
|
||||
"onDelete": "cascade",
|
||||
"onUpdate": "no action"
|
||||
}
|
||||
},
|
||||
"compositePrimaryKeys": {},
|
||||
"uniqueConstraints": {},
|
||||
"policies": {},
|
||||
"checkConstraints": {},
|
||||
"isRLSEnabled": false
|
||||
},
|
||||
"public.friends_list": {
|
||||
"name": "friends_list",
|
||||
"schema": "",
|
||||
"columns": {
|
||||
"time_created": {
|
||||
"name": "time_created",
|
||||
"type": "timestamp with time zone",
|
||||
"primaryKey": false,
|
||||
"notNull": true,
|
||||
"default": "now()"
|
||||
},
|
||||
"time_updated": {
|
||||
"name": "time_updated",
|
||||
"type": "timestamp with time zone",
|
||||
"primaryKey": false,
|
||||
"notNull": true,
|
||||
"default": "now()"
|
||||
},
|
||||
"time_deleted": {
|
||||
"name": "time_deleted",
|
||||
"type": "timestamp with time zone",
|
||||
"primaryKey": false,
|
||||
"notNull": false
|
||||
},
|
||||
"steam_id": {
|
||||
"name": "steam_id",
|
||||
"type": "varchar(255)",
|
||||
"primaryKey": false,
|
||||
"notNull": true
|
||||
},
|
||||
"friend_steam_id": {
|
||||
"name": "friend_steam_id",
|
||||
"type": "varchar(255)",
|
||||
"primaryKey": false,
|
||||
"notNull": true
|
||||
}
|
||||
},
|
||||
"indexes": {},
|
||||
"foreignKeys": {
|
||||
"friends_list_steam_id_steam_accounts_steam_id_fk": {
|
||||
"name": "friends_list_steam_id_steam_accounts_steam_id_fk",
|
||||
"tableFrom": "friends_list",
|
||||
"tableTo": "steam_accounts",
|
||||
"columnsFrom": [
|
||||
"steam_id"
|
||||
],
|
||||
"columnsTo": [
|
||||
"steam_id"
|
||||
],
|
||||
"onDelete": "cascade",
|
||||
"onUpdate": "no action"
|
||||
},
|
||||
"friends_list_friend_steam_id_steam_accounts_steam_id_fk": {
|
||||
"name": "friends_list_friend_steam_id_steam_accounts_steam_id_fk",
|
||||
"tableFrom": "friends_list",
|
||||
"tableTo": "steam_accounts",
|
||||
"columnsFrom": [
|
||||
"friend_steam_id"
|
||||
],
|
||||
"columnsTo": [
|
||||
"steam_id"
|
||||
],
|
||||
"onDelete": "cascade",
|
||||
"onUpdate": "no action"
|
||||
}
|
||||
},
|
||||
"compositePrimaryKeys": {
|
||||
"friends_list_steam_id_friend_steam_id_pk": {
|
||||
"name": "friends_list_steam_id_friend_steam_id_pk",
|
||||
"columns": [
|
||||
"steam_id",
|
||||
"friend_steam_id"
|
||||
]
|
||||
}
|
||||
},
|
||||
"uniqueConstraints": {},
|
||||
"policies": {},
|
||||
"checkConstraints": {},
|
||||
"isRLSEnabled": false
|
||||
},
|
||||
"public.members": {
|
||||
"name": "members",
|
||||
"schema": "",
|
||||
"columns": {
|
||||
"id": {
|
||||
"name": "id",
|
||||
"type": "char(30)",
|
||||
"primaryKey": false,
|
||||
"notNull": true
|
||||
},
|
||||
"team_id": {
|
||||
"name": "team_id",
|
||||
"type": "char(30)",
|
||||
"primaryKey": false,
|
||||
"notNull": true
|
||||
},
|
||||
"time_created": {
|
||||
"name": "time_created",
|
||||
"type": "timestamp with time zone",
|
||||
"primaryKey": false,
|
||||
"notNull": true,
|
||||
"default": "now()"
|
||||
},
|
||||
"time_updated": {
|
||||
"name": "time_updated",
|
||||
"type": "timestamp with time zone",
|
||||
"primaryKey": false,
|
||||
"notNull": true,
|
||||
"default": "now()"
|
||||
},
|
||||
"time_deleted": {
|
||||
"name": "time_deleted",
|
||||
"type": "timestamp with time zone",
|
||||
"primaryKey": false,
|
||||
"notNull": false
|
||||
},
|
||||
"user_id": {
|
||||
"name": "user_id",
|
||||
"type": "char(30)",
|
||||
"primaryKey": false,
|
||||
"notNull": false
|
||||
},
|
||||
"steam_id": {
|
||||
"name": "steam_id",
|
||||
"type": "varchar(255)",
|
||||
"primaryKey": false,
|
||||
"notNull": true
|
||||
},
|
||||
"role": {
|
||||
"name": "role",
|
||||
"type": "member_role",
|
||||
"typeSchema": "public",
|
||||
"primaryKey": false,
|
||||
"notNull": true
|
||||
}
|
||||
},
|
||||
"indexes": {
|
||||
"idx_member_steam_id": {
|
||||
"name": "idx_member_steam_id",
|
||||
"columns": [
|
||||
{
|
||||
"expression": "team_id",
|
||||
"isExpression": false,
|
||||
"asc": true,
|
||||
"nulls": "last"
|
||||
},
|
||||
{
|
||||
"expression": "steam_id",
|
||||
"isExpression": false,
|
||||
"asc": true,
|
||||
"nulls": "last"
|
||||
}
|
||||
],
|
||||
"isUnique": true,
|
||||
"concurrently": false,
|
||||
"method": "btree",
|
||||
"with": {}
|
||||
},
|
||||
"idx_member_user_id": {
|
||||
"name": "idx_member_user_id",
|
||||
"columns": [
|
||||
{
|
||||
"expression": "team_id",
|
||||
"isExpression": false,
|
||||
"asc": true,
|
||||
"nulls": "last"
|
||||
},
|
||||
{
|
||||
"expression": "user_id",
|
||||
"isExpression": false,
|
||||
"asc": true,
|
||||
"nulls": "last"
|
||||
}
|
||||
],
|
||||
"isUnique": true,
|
||||
"where": "\"members\".\"user_id\" is not null",
|
||||
"concurrently": false,
|
||||
"method": "btree",
|
||||
"with": {}
|
||||
}
|
||||
},
|
||||
"foreignKeys": {
|
||||
"members_user_id_users_id_fk": {
|
||||
"name": "members_user_id_users_id_fk",
|
||||
"tableFrom": "members",
|
||||
"tableTo": "users",
|
||||
"columnsFrom": [
|
||||
"user_id"
|
||||
],
|
||||
"columnsTo": [
|
||||
"id"
|
||||
],
|
||||
"onDelete": "cascade",
|
||||
"onUpdate": "no action"
|
||||
},
|
||||
"members_steam_id_steam_accounts_steam_id_fk": {
|
||||
"name": "members_steam_id_steam_accounts_steam_id_fk",
|
||||
"tableFrom": "members",
|
||||
"tableTo": "steam_accounts",
|
||||
"columnsFrom": [
|
||||
"steam_id"
|
||||
],
|
||||
"columnsTo": [
|
||||
"steam_id"
|
||||
],
|
||||
"onDelete": "cascade",
|
||||
"onUpdate": "restrict"
|
||||
}
|
||||
},
|
||||
"compositePrimaryKeys": {
|
||||
"members_id_team_id_pk": {
|
||||
"name": "members_id_team_id_pk",
|
||||
"columns": [
|
||||
"id",
|
||||
"team_id"
|
||||
]
|
||||
}
|
||||
},
|
||||
"uniqueConstraints": {},
|
||||
"policies": {},
|
||||
"checkConstraints": {},
|
||||
"isRLSEnabled": false
|
||||
},
|
||||
"public.steam_accounts": {
|
||||
"name": "steam_accounts",
|
||||
"schema": "",
|
||||
"columns": {
|
||||
"time_created": {
|
||||
"name": "time_created",
|
||||
"type": "timestamp with time zone",
|
||||
"primaryKey": false,
|
||||
"notNull": true,
|
||||
"default": "now()"
|
||||
},
|
||||
"time_updated": {
|
||||
"name": "time_updated",
|
||||
"type": "timestamp with time zone",
|
||||
"primaryKey": false,
|
||||
"notNull": true,
|
||||
"default": "now()"
|
||||
},
|
||||
"time_deleted": {
|
||||
"name": "time_deleted",
|
||||
"type": "timestamp with time zone",
|
||||
"primaryKey": false,
|
||||
"notNull": false
|
||||
},
|
||||
"steam_id": {
|
||||
"name": "steam_id",
|
||||
"type": "varchar(255)",
|
||||
"primaryKey": true,
|
||||
"notNull": true
|
||||
},
|
||||
"user_id": {
|
||||
"name": "user_id",
|
||||
"type": "char(30)",
|
||||
"primaryKey": false,
|
||||
"notNull": false
|
||||
},
|
||||
"status": {
|
||||
"name": "status",
|
||||
"type": "steam_status",
|
||||
"typeSchema": "public",
|
||||
"primaryKey": false,
|
||||
"notNull": true
|
||||
},
|
||||
"last_synced_at": {
|
||||
"name": "last_synced_at",
|
||||
"type": "timestamp with time zone",
|
||||
"primaryKey": false,
|
||||
"notNull": true
|
||||
},
|
||||
"real_name": {
|
||||
"name": "real_name",
|
||||
"type": "varchar(255)",
|
||||
"primaryKey": false,
|
||||
"notNull": false
|
||||
},
|
||||
"member_since": {
|
||||
"name": "member_since",
|
||||
"type": "timestamp with time zone",
|
||||
"primaryKey": false,
|
||||
"notNull": true
|
||||
},
|
||||
"name": {
|
||||
"name": "name",
|
||||
"type": "varchar(255)",
|
||||
"primaryKey": false,
|
||||
"notNull": true
|
||||
},
|
||||
"profile_url": {
|
||||
"name": "profile_url",
|
||||
"type": "varchar(255)",
|
||||
"primaryKey": false,
|
||||
"notNull": false
|
||||
},
|
||||
"username": {
|
||||
"name": "username",
|
||||
"type": "varchar(255)",
|
||||
"primaryKey": false,
|
||||
"notNull": true
|
||||
},
|
||||
"avatar_hash": {
|
||||
"name": "avatar_hash",
|
||||
"type": "varchar(255)",
|
||||
"primaryKey": false,
|
||||
"notNull": true
|
||||
},
|
||||
"limitations": {
|
||||
"name": "limitations",
|
||||
"type": "json",
|
||||
"primaryKey": false,
|
||||
"notNull": true
|
||||
}
|
||||
},
|
||||
"indexes": {},
|
||||
"foreignKeys": {
|
||||
"steam_accounts_user_id_users_id_fk": {
|
||||
"name": "steam_accounts_user_id_users_id_fk",
|
||||
"tableFrom": "steam_accounts",
|
||||
"tableTo": "users",
|
||||
"columnsFrom": [
|
||||
"user_id"
|
||||
],
|
||||
"columnsTo": [
|
||||
"id"
|
||||
],
|
||||
"onDelete": "cascade",
|
||||
"onUpdate": "no action"
|
||||
}
|
||||
},
|
||||
"compositePrimaryKeys": {},
|
||||
"uniqueConstraints": {
|
||||
"idx_steam_username": {
|
||||
"name": "idx_steam_username",
|
||||
"nullsNotDistinct": false,
|
||||
"columns": [
|
||||
"username"
|
||||
]
|
||||
}
|
||||
},
|
||||
"policies": {},
|
||||
"checkConstraints": {},
|
||||
"isRLSEnabled": false
|
||||
},
|
||||
"public.teams": {
|
||||
"name": "teams",
|
||||
"schema": "",
|
||||
"columns": {
|
||||
"id": {
|
||||
"name": "id",
|
||||
"type": "char(30)",
|
||||
"primaryKey": true,
|
||||
"notNull": true
|
||||
},
|
||||
"time_created": {
|
||||
"name": "time_created",
|
||||
"type": "timestamp with time zone",
|
||||
"primaryKey": false,
|
||||
"notNull": true,
|
||||
"default": "now()"
|
||||
},
|
||||
"time_updated": {
|
||||
"name": "time_updated",
|
||||
"type": "timestamp with time zone",
|
||||
"primaryKey": false,
|
||||
"notNull": true,
|
||||
"default": "now()"
|
||||
},
|
||||
"time_deleted": {
|
||||
"name": "time_deleted",
|
||||
"type": "timestamp with time zone",
|
||||
"primaryKey": false,
|
||||
"notNull": false
|
||||
},
|
||||
"name": {
|
||||
"name": "name",
|
||||
"type": "varchar(255)",
|
||||
"primaryKey": false,
|
||||
"notNull": true
|
||||
},
|
||||
"owner_id": {
|
||||
"name": "owner_id",
|
||||
"type": "char(30)",
|
||||
"primaryKey": false,
|
||||
"notNull": true
|
||||
},
|
||||
"invite_code": {
|
||||
"name": "invite_code",
|
||||
"type": "varchar(10)",
|
||||
"primaryKey": false,
|
||||
"notNull": true
|
||||
},
|
||||
"slug": {
|
||||
"name": "slug",
|
||||
"type": "varchar(255)",
|
||||
"primaryKey": false,
|
||||
"notNull": true
|
||||
},
|
||||
"max_members": {
|
||||
"name": "max_members",
|
||||
"type": "bigint",
|
||||
"primaryKey": false,
|
||||
"notNull": true
|
||||
}
|
||||
},
|
||||
"indexes": {
|
||||
"idx_team_slug": {
|
||||
"name": "idx_team_slug",
|
||||
"columns": [
|
||||
{
|
||||
"expression": "slug",
|
||||
"isExpression": false,
|
||||
"asc": true,
|
||||
"nulls": "last"
|
||||
}
|
||||
],
|
||||
"isUnique": true,
|
||||
"concurrently": false,
|
||||
"method": "btree",
|
||||
"with": {}
|
||||
}
|
||||
},
|
||||
"foreignKeys": {
|
||||
"teams_owner_id_users_id_fk": {
|
||||
"name": "teams_owner_id_users_id_fk",
|
||||
"tableFrom": "teams",
|
||||
"tableTo": "users",
|
||||
"columnsFrom": [
|
||||
"owner_id"
|
||||
],
|
||||
"columnsTo": [
|
||||
"id"
|
||||
],
|
||||
"onDelete": "cascade",
|
||||
"onUpdate": "no action"
|
||||
},
|
||||
"teams_slug_steam_accounts_username_fk": {
|
||||
"name": "teams_slug_steam_accounts_username_fk",
|
||||
"tableFrom": "teams",
|
||||
"tableTo": "steam_accounts",
|
||||
"columnsFrom": [
|
||||
"slug"
|
||||
],
|
||||
"columnsTo": [
|
||||
"username"
|
||||
],
|
||||
"onDelete": "cascade",
|
||||
"onUpdate": "no action"
|
||||
}
|
||||
},
|
||||
"compositePrimaryKeys": {},
|
||||
"uniqueConstraints": {
|
||||
"idx_team_invite_code": {
|
||||
"name": "idx_team_invite_code",
|
||||
"nullsNotDistinct": false,
|
||||
"columns": [
|
||||
"invite_code"
|
||||
]
|
||||
}
|
||||
},
|
||||
"policies": {},
|
||||
"checkConstraints": {},
|
||||
"isRLSEnabled": false
|
||||
},
|
||||
"public.users": {
|
||||
"name": "users",
|
||||
"schema": "",
|
||||
"columns": {
|
||||
"id": {
|
||||
"name": "id",
|
||||
"type": "char(30)",
|
||||
"primaryKey": true,
|
||||
"notNull": true
|
||||
},
|
||||
"time_created": {
|
||||
"name": "time_created",
|
||||
"type": "timestamp with time zone",
|
||||
"primaryKey": false,
|
||||
"notNull": true,
|
||||
"default": "now()"
|
||||
},
|
||||
"time_updated": {
|
||||
"name": "time_updated",
|
||||
"type": "timestamp with time zone",
|
||||
"primaryKey": false,
|
||||
"notNull": true,
|
||||
"default": "now()"
|
||||
},
|
||||
"time_deleted": {
|
||||
"name": "time_deleted",
|
||||
"type": "timestamp with time zone",
|
||||
"primaryKey": false,
|
||||
"notNull": false
|
||||
},
|
||||
"email": {
|
||||
"name": "email",
|
||||
"type": "varchar(255)",
|
||||
"primaryKey": false,
|
||||
"notNull": true
|
||||
},
|
||||
"avatar_url": {
|
||||
"name": "avatar_url",
|
||||
"type": "text",
|
||||
"primaryKey": false,
|
||||
"notNull": false
|
||||
},
|
||||
"last_login": {
|
||||
"name": "last_login",
|
||||
"type": "timestamp with time zone",
|
||||
"primaryKey": false,
|
||||
"notNull": true
|
||||
},
|
||||
"name": {
|
||||
"name": "name",
|
||||
"type": "varchar(255)",
|
||||
"primaryKey": false,
|
||||
"notNull": true
|
||||
},
|
||||
"polar_customer_id": {
|
||||
"name": "polar_customer_id",
|
||||
"type": "varchar(255)",
|
||||
"primaryKey": false,
|
||||
"notNull": false
|
||||
}
|
||||
},
|
||||
"indexes": {},
|
||||
"foreignKeys": {},
|
||||
"compositePrimaryKeys": {},
|
||||
"uniqueConstraints": {
|
||||
"idx_user_email": {
|
||||
"name": "idx_user_email",
|
||||
"nullsNotDistinct": false,
|
||||
"columns": [
|
||||
"email"
|
||||
]
|
||||
}
|
||||
},
|
||||
"policies": {},
|
||||
"checkConstraints": {},
|
||||
"isRLSEnabled": false
|
||||
}
|
||||
},
|
||||
"enums": {
|
||||
"public.member_role": {
|
||||
"name": "member_role",
|
||||
"schema": "public",
|
||||
"values": [
|
||||
"child",
|
||||
"adult"
|
||||
]
|
||||
},
|
||||
"public.steam_status": {
|
||||
"name": "steam_status",
|
||||
"schema": "public",
|
||||
"values": [
|
||||
"online",
|
||||
"offline",
|
||||
"dnd",
|
||||
"playing"
|
||||
]
|
||||
}
|
||||
},
|
||||
"schemas": {},
|
||||
"sequences": {},
|
||||
"roles": {},
|
||||
"policies": {},
|
||||
"views": {},
|
||||
"_meta": {
|
||||
"columns": {},
|
||||
"schemas": {},
|
||||
"tables": {}
|
||||
}
|
||||
}
|
||||
@@ -71,6 +71,13 @@
|
||||
"when": 1744651817581,
|
||||
"tag": "0009_luxuriant_wraith",
|
||||
"breakpoints": true
|
||||
},
|
||||
{
|
||||
"idx": 10,
|
||||
"version": "7",
|
||||
"when": 1746726715456,
|
||||
"tag": "0010_certain_dust",
|
||||
"breakpoints": true
|
||||
}
|
||||
]
|
||||
}
|
||||
@@ -17,24 +17,22 @@
|
||||
"@tsconfig/node20": "^20.1.4",
|
||||
"aws-iot-device-sdk-v2": "^1.21.1",
|
||||
"aws4fetch": "^1.0.20",
|
||||
"loops": "^3.4.1",
|
||||
"mqtt": "^5.10.3",
|
||||
"remeda": "^2.21.2",
|
||||
"ulid": "^2.3.0",
|
||||
"uuid": "^11.0.3",
|
||||
"zod": "^3.24.1",
|
||||
"zod-openapi": "^4.2.2"
|
||||
},
|
||||
"dependencies": {
|
||||
"@aws-sdk/client-iot-data-plane": "^3.758.0",
|
||||
"@aws-sdk/client-rds-data": "^3.758.0",
|
||||
"@aws-sdk/client-sesv2": "^3.753.0",
|
||||
"@instantdb/admin": "^0.17.7",
|
||||
"@openauthjs/openauth": "*",
|
||||
"@openauthjs/openevent": "^0.0.27",
|
||||
"@polar-sh/sdk": "^0.26.1",
|
||||
"drizzle-kit": "^0.30.5",
|
||||
"drizzle-orm": "^0.40.0",
|
||||
"postgres": "^3.4.5"
|
||||
"drizzle-zod": "^0.7.1",
|
||||
"postgres": "^3.4.5",
|
||||
"steam-session": "*"
|
||||
}
|
||||
}
|
||||
@@ -30,7 +30,7 @@ export namespace Actor {
|
||||
export interface Token {
|
||||
type: "steam";
|
||||
properties: {
|
||||
steamID: bigint;
|
||||
steamID: string;
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
20
packages/core/src/credentials/credentials.sql.ts
Normal file
20
packages/core/src/credentials/credentials.sql.ts
Normal file
@@ -0,0 +1,20 @@
|
||||
import { steamTable } from "../steam/steam.sql";
|
||||
import { pgTable, varchar } from "drizzle-orm/pg-core";
|
||||
import { encryptedText, timestamps, utc } from "../drizzle/types";
|
||||
|
||||
export const steamCredentialsTable = pgTable(
|
||||
"steam_account_credentials",
|
||||
{
|
||||
...timestamps,
|
||||
id: varchar("steam_id", { length: 255 })
|
||||
.notNull()
|
||||
.primaryKey()
|
||||
.references(() => steamTable.id, {
|
||||
onDelete: "cascade"
|
||||
}),
|
||||
refreshToken: encryptedText("refresh_token")
|
||||
.notNull(),
|
||||
expiry: utc("expiry").notNull(),
|
||||
username: varchar("username", { length: 255 }).notNull(),
|
||||
}
|
||||
)
|
||||
115
packages/core/src/credentials/index.ts
Normal file
115
packages/core/src/credentials/index.ts
Normal file
@@ -0,0 +1,115 @@
|
||||
import { z } from "zod";
|
||||
import { createID, fn } from "../utils";
|
||||
import { Resource } from "sst";
|
||||
import { bus } from "sst/aws/bus";
|
||||
import { createEvent } from "../event";
|
||||
import { eq, and, isNull, gt } from "drizzle-orm";
|
||||
import { createSelectSchema } from "drizzle-zod";
|
||||
import { steamCredentialsTable } from "./credentials.sql";
|
||||
import { afterTx, createTransaction, useTransaction } from "../drizzle/transaction";
|
||||
|
||||
export namespace Credentials {
|
||||
export const Info = createSelectSchema(steamCredentialsTable)
|
||||
.omit({ timeCreated: true, timeDeleted: true, timeUpdated: true })
|
||||
.extend({
|
||||
accessToken: z.string(),
|
||||
cookies: z.string().array()
|
||||
})
|
||||
|
||||
export type Info = z.infer<typeof Info>;
|
||||
|
||||
export const Events = {
|
||||
New: createEvent(
|
||||
"new_credentials.added",
|
||||
z.object({
|
||||
steamID: Info.shape.id,
|
||||
}),
|
||||
),
|
||||
};
|
||||
|
||||
export const create = fn(
|
||||
Info
|
||||
.omit({ accessToken: true, cookies: true, expiry: true }),
|
||||
(input) => {
|
||||
const part = input.refreshToken.split('.')[1] as string
|
||||
|
||||
const payload = JSON.parse(Buffer.from(part, 'base64').toString());
|
||||
|
||||
return createTransaction(async (tx) => {
|
||||
const id = input.id
|
||||
await tx
|
||||
.insert(steamCredentialsTable)
|
||||
.values({
|
||||
id,
|
||||
username: input.username,
|
||||
refreshToken: input.refreshToken,
|
||||
expiry: new Date(payload.exp * 1000),
|
||||
})
|
||||
// await afterTx(async () =>
|
||||
// await bus.publish(Resource.Bus, Events.New, { steamID: input.id })
|
||||
// );
|
||||
return id
|
||||
})
|
||||
});
|
||||
|
||||
export const getByID = fn(
|
||||
Info.shape.id,
|
||||
(id) =>
|
||||
useTransaction(async (tx) => {
|
||||
const now = new Date()
|
||||
|
||||
const credential = await tx
|
||||
.select()
|
||||
.from(steamCredentialsTable)
|
||||
.where(
|
||||
and(
|
||||
eq(steamCredentialsTable.id, id),
|
||||
isNull(steamCredentialsTable.timeDeleted),
|
||||
gt(steamCredentialsTable.expiry, now)
|
||||
)
|
||||
)
|
||||
.execute()
|
||||
.then(rows => rows.at(0));
|
||||
|
||||
if (!credential) return null;
|
||||
|
||||
return serialize(credential);
|
||||
})
|
||||
);
|
||||
|
||||
// export const getBySteamID = fn(
|
||||
// Info.shape.steamID,
|
||||
// (steamID) =>
|
||||
// useTransaction(async (tx) => {
|
||||
// const now = new Date()
|
||||
|
||||
// const credential = await tx
|
||||
// .select()
|
||||
// .from(steamCredentialsTable)
|
||||
// .where(
|
||||
// and(
|
||||
// eq(steamCredentialsTable.steamID, steamID),
|
||||
// isNull(steamCredentialsTable.timeDeleted),
|
||||
// gt(steamCredentialsTable.expiry, now)
|
||||
// )
|
||||
// )
|
||||
// .execute()
|
||||
// .then(rows => rows.at(0));
|
||||
|
||||
// if (!credential) return null;
|
||||
|
||||
// return serialize(credential);
|
||||
// })
|
||||
// );
|
||||
|
||||
export function serialize(
|
||||
input: typeof steamCredentialsTable.$inferSelect,
|
||||
) {
|
||||
return {
|
||||
id: input.id,
|
||||
expiry: input.expiry,
|
||||
username: input.username,
|
||||
refreshToken: input.refreshToken,
|
||||
};
|
||||
}
|
||||
}
|
||||
@@ -1,5 +1,5 @@
|
||||
import { char, timestamp as rawTs } from "drizzle-orm/pg-core";
|
||||
import { teamTable } from "../team/team.sql";
|
||||
import { Token } from "../utils";
|
||||
import { char, customType, timestamp as rawTs } from "drizzle-orm/pg-core";
|
||||
|
||||
export const ulid = (name: string) => char(name, { length: 26 + 4 });
|
||||
|
||||
@@ -33,6 +33,19 @@ export const utc = (name: string) =>
|
||||
// mode: "date"
|
||||
});
|
||||
|
||||
export const encryptedText =
|
||||
customType<{ data: string; driverData: string; }>({
|
||||
dataType() {
|
||||
return 'text';
|
||||
},
|
||||
fromDriver(val) {
|
||||
return Token.decrypt(val);
|
||||
},
|
||||
toDriver(val) {
|
||||
return Token.encrypt(val);
|
||||
},
|
||||
});
|
||||
|
||||
export const timestamps = {
|
||||
timeCreated: utc("time_created").notNull().defaultNow(),
|
||||
timeUpdated: utc("time_updated").notNull().defaultNow(),
|
||||
|
||||
@@ -44,7 +44,7 @@ export namespace Examples {
|
||||
accountStatus: "new" as const, //active or pending
|
||||
limitations: {
|
||||
isLimited: false,
|
||||
isTradeBanned: false,
|
||||
tradeBanState: "none" as const,
|
||||
isVacBanned: false,
|
||||
visibilityState: 3,
|
||||
privacyState: "public" as const,
|
||||
|
||||
25
packages/core/src/friend/friend.sql.ts
Normal file
25
packages/core/src/friend/friend.sql.ts
Normal file
@@ -0,0 +1,25 @@
|
||||
import { timestamps, } from "../drizzle/types";
|
||||
import { steamTable } from "../steam/steam.sql";
|
||||
import { pgTable,primaryKey, varchar } from "drizzle-orm/pg-core";
|
||||
|
||||
export const friendTable = pgTable(
|
||||
"friends_list",
|
||||
{
|
||||
...timestamps,
|
||||
steamID: varchar("steam_id", { length: 255 })
|
||||
.notNull()
|
||||
.references(() => steamTable.id, {
|
||||
onDelete: "cascade"
|
||||
}),
|
||||
friendSteamID: varchar("friend_steam_id", { length: 255 })
|
||||
.notNull()
|
||||
.references(() => steamTable.id, {
|
||||
onDelete: "cascade"
|
||||
}),
|
||||
},
|
||||
(table) => [
|
||||
primaryKey({
|
||||
columns: [table.steamID, table.friendSteamID]
|
||||
}),
|
||||
]
|
||||
);
|
||||
166
packages/core/src/friend/index.ts
Normal file
166
packages/core/src/friend/index.ts
Normal file
@@ -0,0 +1,166 @@
|
||||
import { z } from "zod";
|
||||
import { fn } from "../utils";
|
||||
import { User } from "../user";
|
||||
import { Steam } from "../steam";
|
||||
import { Actor } from "../actor";
|
||||
import { Examples } from "../examples";
|
||||
import { friendTable } from "./friend.sql";
|
||||
import { userTable } from "../user/user.sql";
|
||||
import { steamTable } from "../steam/steam.sql";
|
||||
import { createSelectSchema } from "drizzle-zod";
|
||||
import { and, eq, isNull, sql } from "drizzle-orm";
|
||||
import { groupBy, map, pipe, values } from "remeda";
|
||||
import { createTransaction, useTransaction } from "../drizzle/transaction";
|
||||
import { ErrorCodes, VisibleError } from "../error";
|
||||
|
||||
export namespace Friend {
|
||||
export const Info = Steam.Info
|
||||
.extend({
|
||||
user: User.Info.nullable().openapi({
|
||||
description: "The user account that owns this Steam account",
|
||||
example: Examples.User
|
||||
})
|
||||
})
|
||||
.openapi({
|
||||
ref: "Friend",
|
||||
description: "Represents a friend's information stored on Nestri",
|
||||
example: { ...Examples.SteamAccount, user: Examples.User },
|
||||
});
|
||||
|
||||
export const InputInfo = createSelectSchema(friendTable)
|
||||
.omit({ timeCreated: true, timeDeleted: true, timeUpdated: true })
|
||||
|
||||
export type InputInfo = z.infer<typeof InputInfo>;
|
||||
|
||||
export const add = fn(
|
||||
InputInfo.partial({ steamID: true }),
|
||||
async (input) =>
|
||||
createTransaction(async (tx) => {
|
||||
const steamID = input.steamID ?? Actor.steamID()
|
||||
if (steamID === input.friendSteamID) {
|
||||
throw new VisibleError(
|
||||
"forbidden",
|
||||
ErrorCodes.Validation.INVALID_PARAMETER,
|
||||
"Cannot add yourself as a friend"
|
||||
);
|
||||
}
|
||||
|
||||
await tx
|
||||
.insert(friendTable)
|
||||
.values({
|
||||
steamID,
|
||||
friendSteamID: input.friendSteamID
|
||||
})
|
||||
.onConflictDoUpdate({
|
||||
target: [friendTable.steamID, friendTable.friendSteamID],
|
||||
set: { timeDeleted: null }
|
||||
})
|
||||
|
||||
return steamID
|
||||
}),
|
||||
)
|
||||
|
||||
export const end = fn(
|
||||
InputInfo,
|
||||
(input) =>
|
||||
useTransaction(async (tx) =>
|
||||
tx
|
||||
.update(friendTable)
|
||||
.set({ timeDeleted: sql`now()` })
|
||||
.where(
|
||||
and(
|
||||
eq(friendTable.steamID, input.steamID),
|
||||
eq(friendTable.friendSteamID, input.friendSteamID),
|
||||
)
|
||||
)
|
||||
)
|
||||
)
|
||||
|
||||
export const list = async () =>
|
||||
useTransaction(async (tx) => {
|
||||
const userSteamAccounts =
|
||||
await tx
|
||||
.select()
|
||||
.from(steamTable)
|
||||
.where(eq(steamTable.userID, Actor.userID()))
|
||||
.execute();
|
||||
|
||||
if (userSteamAccounts.length === 0) {
|
||||
return []; // User has no steam accounts
|
||||
}
|
||||
|
||||
const friendPromises =
|
||||
userSteamAccounts.map(async (steamAccount) => {
|
||||
return await fromSteamID(steamAccount.id)
|
||||
})
|
||||
|
||||
return (await Promise.all(friendPromises)).flat()
|
||||
})
|
||||
|
||||
export const fromSteamID = fn(
|
||||
InputInfo.shape.steamID,
|
||||
(steamID) =>
|
||||
useTransaction(async (tx) =>
|
||||
tx
|
||||
.select({
|
||||
steam: steamTable,
|
||||
user: userTable
|
||||
})
|
||||
.from(friendTable)
|
||||
.innerJoin(
|
||||
steamTable,
|
||||
eq(friendTable.friendSteamID, steamTable.id)
|
||||
)
|
||||
.leftJoin(
|
||||
userTable,
|
||||
eq(steamTable.userID, userTable.id)
|
||||
)
|
||||
.where(
|
||||
and(
|
||||
eq(friendTable.steamID, steamID),
|
||||
isNull(friendTable.timeDeleted)
|
||||
)
|
||||
)
|
||||
.orderBy(friendTable.timeCreated)
|
||||
.limit(100)
|
||||
.execute()
|
||||
.then((rows) => serialize(rows))
|
||||
)
|
||||
)
|
||||
|
||||
export const areFriends = fn(
|
||||
InputInfo,
|
||||
(input) =>
|
||||
useTransaction(async (tx) => {
|
||||
const result = await tx
|
||||
.select()
|
||||
.from(friendTable)
|
||||
.where(
|
||||
and(
|
||||
eq(friendTable.steamID, input.steamID),
|
||||
eq(friendTable.friendSteamID, input.friendSteamID),
|
||||
isNull(friendTable.timeDeleted)
|
||||
)
|
||||
)
|
||||
.limit(1)
|
||||
.execute()
|
||||
|
||||
return result.length > 0
|
||||
})
|
||||
)
|
||||
|
||||
export function serialize(
|
||||
input: { user: typeof userTable.$inferSelect | null; steam: typeof steamTable.$inferSelect }[],
|
||||
): z.infer<typeof Info>[] {
|
||||
return pipe(
|
||||
input,
|
||||
groupBy((row) => row.steam.id.toString()),
|
||||
values(),
|
||||
map((group) => ({
|
||||
...Steam.serialize(group[0].steam),
|
||||
user: group[0].user ? User.serialize(group[0].user!) : null
|
||||
}))
|
||||
)
|
||||
}
|
||||
|
||||
}
|
||||
@@ -7,8 +7,9 @@ import { Common } from "../common";
|
||||
import { createEvent } from "../event";
|
||||
import { Examples } from "../examples";
|
||||
import { eq, and, isNull, desc } from "drizzle-orm";
|
||||
import { steamTable, StatusEnum, Limitations } from "./steam.sql";
|
||||
import { afterTx, createTransaction, useTransaction } from "../drizzle/transaction";
|
||||
import { steamTable, StatusEnum, AccountStatusEnum, Limitations } from "./steam.sql";
|
||||
import { teamTable } from "../team/team.sql";
|
||||
|
||||
export namespace Steam {
|
||||
export const Info = z
|
||||
@@ -25,10 +26,6 @@ export namespace Steam {
|
||||
description: "The current connection status of this Steam account",
|
||||
example: Examples.SteamAccount.status
|
||||
}),
|
||||
accountStatus: z.enum(AccountStatusEnum.enumValues).openapi({
|
||||
description: "The current status of this Steam account",
|
||||
example: Examples.SteamAccount.accountStatus
|
||||
}),
|
||||
userID: z.string().nullable().openapi({
|
||||
description: "The user id of which account owns this steam account",
|
||||
example: Examples.SteamAccount.userID
|
||||
@@ -45,7 +42,7 @@ export namespace Steam {
|
||||
example: Examples.SteamAccount.username
|
||||
})
|
||||
.default("unknown"),
|
||||
realName: z.string().openapi({
|
||||
realName: z.string().nullable().openapi({
|
||||
description: "The real name behind of this Steam account",
|
||||
example: Examples.SteamAccount.realName
|
||||
}),
|
||||
@@ -100,7 +97,6 @@ export namespace Steam {
|
||||
useUser: true,
|
||||
userID: true,
|
||||
status: true,
|
||||
accountStatus: true,
|
||||
lastSyncedAt: true
|
||||
}),
|
||||
(input) =>
|
||||
@@ -131,67 +127,65 @@ export namespace Steam {
|
||||
realName: input.realName,
|
||||
profileUrl: input.profileUrl,
|
||||
avatarHash: input.avatarHash,
|
||||
steamMemberSince: input.steamMemberSince,
|
||||
limitations: input.limitations,
|
||||
status: input.status ?? "offline",
|
||||
username: input.username ?? "unknown",
|
||||
accountStatus: input.accountStatus ?? "new",
|
||||
steamMemberSince: input.steamMemberSince,
|
||||
lastSyncedAt: input.lastSyncedAt ?? Common.utc(),
|
||||
})
|
||||
|
||||
await afterTx(async () =>
|
||||
bus.publish(Resource.Bus, Events.Created, { userID, steamID: input.id })
|
||||
);
|
||||
// await afterTx(async () =>
|
||||
// bus.publish(Resource.Bus, Events.Created, { userID, steamID: input.id })
|
||||
// );
|
||||
|
||||
return input.id
|
||||
}),
|
||||
);
|
||||
|
||||
export const update = fn(
|
||||
Info
|
||||
.extend({
|
||||
useUser: z.boolean(),
|
||||
})
|
||||
.partial({
|
||||
useUser: true,
|
||||
userID: true,
|
||||
status: true,
|
||||
lastSyncedAt: true,
|
||||
avatarHash: true,
|
||||
username: true,
|
||||
realName: true,
|
||||
limitations: true,
|
||||
accountStatus: true,
|
||||
name: true,
|
||||
profileUrl: true,
|
||||
steamMemberSince: true,
|
||||
}),
|
||||
async (input) =>
|
||||
useTransaction(async (tx) => {
|
||||
const userID = typeof input.userID === "string" ? input.userID : input.useUser ? Actor.userID() : undefined;
|
||||
await tx
|
||||
.update(steamTable)
|
||||
.set({
|
||||
userID,
|
||||
id: input.id,
|
||||
name: input.name,
|
||||
realName: input.realName,
|
||||
profileUrl: input.profileUrl,
|
||||
avatarHash: input.avatarHash,
|
||||
limitations: input.limitations,
|
||||
status: input.status ?? "offline",
|
||||
username: input.username ?? "unknown",
|
||||
steamMemberSince: input.steamMemberSince,
|
||||
accountStatus: input.accountStatus ?? "new",
|
||||
lastSyncedAt: input.lastSyncedAt ?? Common.utc(),
|
||||
})
|
||||
.where(eq(steamTable.id, input.id));
|
||||
// TODO: This needs to be handled better, as it has the potential to turn unnecessary fields into `null`
|
||||
// export const update = fn(
|
||||
// Info
|
||||
// .extend({
|
||||
// useUser: z.boolean(),
|
||||
// })
|
||||
// .partial({
|
||||
// useUser: true,
|
||||
// userID: true,
|
||||
// status: true,
|
||||
// name: true,
|
||||
// lastSyncedAt: true,
|
||||
// avatarHash: true,
|
||||
// username: true,
|
||||
// realName: true,
|
||||
// limitations: true,
|
||||
// profileUrl: true,
|
||||
// steamMemberSince: true,
|
||||
// }),
|
||||
// async (input) =>
|
||||
// useTransaction(async (tx) => {
|
||||
// const userID = typeof input.userID === "string" ? input.userID : input.useUser ? Actor.userID() : undefined;
|
||||
// await tx
|
||||
// .update(steamTable)
|
||||
// .set({
|
||||
// userID,
|
||||
// id: input.id,
|
||||
// name: input.name,
|
||||
// realName: input.realName,
|
||||
// profileUrl: input.profileUrl,
|
||||
// avatarHash: input.avatarHash,
|
||||
// limitations: input.limitations,
|
||||
// status: input.status ?? "offline",
|
||||
// username: input.username ?? "unknown",
|
||||
// steamMemberSince: input.steamMemberSince,
|
||||
// lastSyncedAt: input.lastSyncedAt ?? Common.utc(),
|
||||
// })
|
||||
// .where(eq(steamTable.id, input.id));
|
||||
|
||||
await afterTx(async () =>
|
||||
bus.publish(Resource.Bus, Events.Updated, { userID: userID ?? null, steamID: input.id })
|
||||
);
|
||||
})
|
||||
)
|
||||
// await afterTx(async () =>
|
||||
// bus.publish(Resource.Bus, Events.Updated, { userID: userID ?? null, steamID: input.id })
|
||||
// );
|
||||
// })
|
||||
// )
|
||||
|
||||
export const fromUserID = fn(
|
||||
z.string().min(1),
|
||||
@@ -245,7 +239,6 @@ export namespace Steam {
|
||||
avatarHash: input.avatarHash,
|
||||
limitations: input.limitations,
|
||||
lastSyncedAt: input.lastSyncedAt,
|
||||
accountStatus: input.accountStatus,
|
||||
steamMemberSince: input.steamMemberSince,
|
||||
profileUrl: input.profileUrl ? `https://steamcommunity.com/id/${input.profileUrl}` : null,
|
||||
};
|
||||
|
||||
@@ -1,17 +1,16 @@
|
||||
import { z } from "zod";
|
||||
import { userTable } from "../user/user.sql";
|
||||
import { timestamps, ulid, utc } from "../drizzle/types";
|
||||
import { pgTable, varchar, text, bigint, pgEnum, json, unique } from "drizzle-orm/pg-core";
|
||||
import { pgTable, varchar, pgEnum, json, unique } from "drizzle-orm/pg-core";
|
||||
|
||||
export const AccountStatusEnum = pgEnum("steam_account_status", ["new", "pending", "active"])
|
||||
export const StatusEnum = pgEnum("steam_status", ["online", "offline", "dnd", "playing"])
|
||||
|
||||
export const Limitations = z.object({
|
||||
isLimited: z.boolean(),
|
||||
isTradeBanned: z.boolean(),
|
||||
tradeBanState: z.enum(["none", "probation", "banned"]),
|
||||
isVacBanned: z.boolean(),
|
||||
visibilityState: z.number(),
|
||||
privacyState: z.enum(["public", "private"]),
|
||||
privacyState: z.enum(["public", "private", "friendsfriendsonly", "friendsonly"]),
|
||||
})
|
||||
|
||||
export type Limitations = z.infer<typeof Limitations>;
|
||||
@@ -29,33 +28,15 @@ export const steamTable = pgTable(
|
||||
}),
|
||||
status: StatusEnum("status").notNull(),
|
||||
lastSyncedAt: utc("last_synced_at").notNull(),
|
||||
realName: varchar("real_name", { length: 255 }),
|
||||
steamMemberSince: utc("member_since").notNull(),
|
||||
name: varchar("name", { length: 255 }).notNull(),
|
||||
profileUrl: varchar("profileUrl", { length: 255 }),
|
||||
profileUrl: varchar("profile_url", { length: 255 }),
|
||||
username: varchar("username", { length: 255 }).notNull(),
|
||||
realName: varchar("real_name", { length: 255 }).notNull(),
|
||||
accountStatus: AccountStatusEnum("account_status").notNull(),
|
||||
avatarHash: varchar("avatar_hash", { length: 255 }).notNull(),
|
||||
limitations: json("limitations").$type<Limitations>().notNull(),
|
||||
},
|
||||
(table) => [
|
||||
unique("idx_steam_username").on(table.username)
|
||||
]
|
||||
);
|
||||
|
||||
// export const steamCredentialsTable = pgTable(
|
||||
// "steam_account_credentials",
|
||||
// {
|
||||
// ...timestamps,
|
||||
// refreshToken: text("refresh_token")
|
||||
// .notNull(),
|
||||
// expiry: utc("expiry").notNull(),
|
||||
// id: bigint("steam_id", { mode: "bigint" })
|
||||
// .notNull()
|
||||
// .primaryKey()
|
||||
// .references(() => steamTable.id, {
|
||||
// onDelete: "cascade"
|
||||
// }),
|
||||
// username: varchar("username", { length: 255 }).notNull(),
|
||||
// }
|
||||
// )
|
||||
);
|
||||
@@ -100,10 +100,6 @@ export namespace User {
|
||||
if (result.count === 0) {
|
||||
throw new UserExistsError(input.email)
|
||||
}
|
||||
|
||||
await afterTx(async () =>
|
||||
bus.publish(Resource.Bus, Events.Created, { userID: id })
|
||||
);
|
||||
})
|
||||
|
||||
return id;
|
||||
|
||||
@@ -2,6 +2,7 @@ import { ulid } from "ulid";
|
||||
|
||||
export const prefixes = {
|
||||
user: "usr",
|
||||
credentials:"crd",
|
||||
team: "tem",
|
||||
product: "prd",
|
||||
session: "ses",
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
export * from "./fn"
|
||||
export * from "./log"
|
||||
export * from "./id"
|
||||
export * from "./invite"
|
||||
export * from "./invite"
|
||||
export * from "./token"
|
||||
58
packages/core/src/utils/token.ts
Normal file
58
packages/core/src/utils/token.ts
Normal file
@@ -0,0 +1,58 @@
|
||||
import { z } from 'zod';
|
||||
import { fn } from './fn';
|
||||
import crypto from 'crypto';
|
||||
import { Resource } from 'sst';
|
||||
|
||||
// This is a 32-character random ASCII string
|
||||
const rawKey = Resource.SteamEncryptionKey.value;
|
||||
|
||||
// Turn it into exactly 32 bytes via UTF-8
|
||||
const key = Buffer.from(rawKey, 'utf8');
|
||||
if (key.length !== 32) {
|
||||
throw new Error(
|
||||
`SteamEncryptionKey must be exactly 32 bytes; got ${key.length}`
|
||||
);
|
||||
}
|
||||
|
||||
const ENCRYPTION_IV_LENGTH = 12; // 96 bits for GCM
|
||||
|
||||
export namespace Token {
|
||||
export const encrypt = fn(
|
||||
z.string().min(4),
|
||||
(token) => {
|
||||
const iv = crypto.randomBytes(ENCRYPTION_IV_LENGTH);
|
||||
const cipher = crypto.createCipheriv('aes-256-gcm', key, iv);
|
||||
|
||||
const ciphertext = Buffer.concat([
|
||||
cipher.update(token, 'utf8'),
|
||||
cipher.final(),
|
||||
]);
|
||||
const tag = cipher.getAuthTag();
|
||||
|
||||
return ['v1', iv.toString('hex'), tag.toString('hex'), ciphertext.toString('hex')].join(':');
|
||||
});
|
||||
|
||||
export const decrypt = fn(
|
||||
z.string().min(4),
|
||||
(data) => {
|
||||
const [version, ivHex, tagHex, ciphertextHex] = data.split(':');
|
||||
if (version !== 'v1' || !ivHex || !tagHex || !ciphertextHex) {
|
||||
throw new Error('Invalid token format');
|
||||
}
|
||||
|
||||
const iv = Buffer.from(ivHex, 'hex');
|
||||
const tag = Buffer.from(tagHex, 'hex');
|
||||
const ciphertext = Buffer.from(ciphertextHex, 'hex');
|
||||
|
||||
const decipher = crypto.createDecipheriv('aes-256-gcm', key, iv);
|
||||
decipher.setAuthTag(tag);
|
||||
|
||||
const plaintext = Buffer.concat([
|
||||
decipher.update(ciphertext),
|
||||
decipher.final(),
|
||||
]);
|
||||
|
||||
return plaintext.toString('utf8');
|
||||
});
|
||||
|
||||
}
|
||||
Reference in New Issue
Block a user