mirror of
https://github.com/nestriness/nestri.git
synced 2025-12-13 09:15:37 +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:
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
|
||||
}))
|
||||
)
|
||||
}
|
||||
|
||||
}
|
||||
Reference in New Issue
Block a user