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:
Wanjohi
2025-05-09 01:13:44 +03:00
committed by GitHub
parent 70d629227a
commit 7e69af977b
51 changed files with 2332 additions and 2805 deletions

View File

@@ -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,
};

View File

@@ -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(),
// }
// )
);