mirror of
https://github.com/nestriness/nestri.git
synced 2025-12-12 08:45:38 +02:00
## 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 -->
188 lines
5.8 KiB
TypeScript
188 lines
5.8 KiB
TypeScript
import { z } from "zod";
|
|
import { Resource } from "sst";
|
|
import { bus } from "sst/aws/bus";
|
|
import { Common } from "../common";
|
|
import { createEvent } from "../event";
|
|
import { Polar } from "../polar/index";
|
|
import { createID, fn } from "../utils";
|
|
import { userTable } from "./user.sql";
|
|
import { Examples } from "../examples";
|
|
import { and, eq, isNull, asc} from "drizzle-orm";
|
|
import { ErrorCodes, VisibleError } from "../error";
|
|
import { afterTx, createTransaction, useTransaction } from "../drizzle/transaction";
|
|
|
|
export namespace User {
|
|
export const Info = z
|
|
.object({
|
|
id: z.string().openapi({
|
|
description: Common.IdDescription,
|
|
example: Examples.User.id,
|
|
}),
|
|
name: z.string().regex(/^[a-zA-Z ]{1,32}$/, "Use a friendly name.").openapi({
|
|
description: "The name of this account",
|
|
example: Examples.User.name
|
|
}),
|
|
polarCustomerID: z.string().nullable().openapi({
|
|
description: "Associated Polar.sh customer identifier",
|
|
example: Examples.User.polarCustomerID,
|
|
}),
|
|
avatarUrl: z.string().url().nullable().openapi({
|
|
description: "The url to the profile picture",
|
|
example: Examples.User.avatarUrl
|
|
}),
|
|
email: z.string().openapi({
|
|
description: "Primary email address for user notifications and authentication",
|
|
example: Examples.User.email,
|
|
}),
|
|
lastLogin: z.date().openapi({
|
|
description: "Timestamp of user's most recent authentication",
|
|
example: Examples.User.lastLogin
|
|
})
|
|
})
|
|
.openapi({
|
|
ref: "User",
|
|
description: "User account entity with core identification and authentication details",
|
|
example: Examples.User,
|
|
});
|
|
|
|
export type Info = z.infer<typeof Info>;
|
|
|
|
export class UserExistsError extends VisibleError {
|
|
constructor(username: string) {
|
|
super(
|
|
"already_exists",
|
|
ErrorCodes.Validation.ALREADY_EXISTS,
|
|
`A user with this email ${username} already exists`
|
|
);
|
|
}
|
|
}
|
|
|
|
export const Events = {
|
|
Created: createEvent(
|
|
"user.created",
|
|
z.object({
|
|
userID: Info.shape.id,
|
|
}),
|
|
),
|
|
};
|
|
|
|
export const create = fn(
|
|
Info
|
|
.omit({
|
|
lastLogin: true,
|
|
polarCustomerID: true,
|
|
}).partial({
|
|
avatarUrl: true,
|
|
id: true
|
|
}),
|
|
async (input) => {
|
|
const userID = createID("user")
|
|
|
|
const customer = await Polar.fromUserEmail(input.email)
|
|
|
|
const id = input.id ?? userID;
|
|
|
|
await createTransaction(async (tx) => {
|
|
const result = await tx
|
|
.insert(userTable)
|
|
.values({
|
|
id,
|
|
avatarUrl: input.avatarUrl,
|
|
email: input.email,
|
|
name: input.name,
|
|
polarCustomerID: customer?.id,
|
|
lastLogin: Common.utc()
|
|
})
|
|
.onConflictDoNothing({
|
|
target: [userTable.email]
|
|
})
|
|
|
|
if (result.count === 0) {
|
|
throw new UserExistsError(input.email)
|
|
}
|
|
})
|
|
|
|
return id;
|
|
})
|
|
|
|
export const fromEmail = fn(
|
|
Info.shape.email.min(1),
|
|
async (email) =>
|
|
useTransaction(async (tx) =>
|
|
tx
|
|
.select()
|
|
.from(userTable)
|
|
.where(
|
|
and(
|
|
eq(userTable.email, email),
|
|
isNull(userTable.timeDeleted)
|
|
)
|
|
)
|
|
.orderBy(asc(userTable.timeCreated))
|
|
.execute()
|
|
.then(rows => rows.map(serialize).at(0))
|
|
)
|
|
)
|
|
|
|
export const fromID = fn(
|
|
Info.shape.id.min(1),
|
|
(id) =>
|
|
useTransaction(async (tx) =>
|
|
tx
|
|
.select()
|
|
.from(userTable)
|
|
.where(
|
|
and(
|
|
eq(userTable.id, id),
|
|
isNull(userTable.timeDeleted)
|
|
)
|
|
)
|
|
.orderBy(asc(userTable.timeCreated))
|
|
.execute()
|
|
.then(rows => rows.map(serialize).at(0))
|
|
),
|
|
)
|
|
|
|
export const remove = fn(
|
|
Info.shape.id.min(1),
|
|
(id) =>
|
|
useTransaction(async (tx) => {
|
|
await tx
|
|
.update(userTable)
|
|
.set({
|
|
timeDeleted: Common.utc(),
|
|
})
|
|
.where(and(eq(userTable.id, id)))
|
|
.execute();
|
|
return id;
|
|
}),
|
|
);
|
|
|
|
export const acknowledgeLogin = fn(
|
|
Info.shape.id,
|
|
(id) =>
|
|
useTransaction(async (tx) =>
|
|
tx
|
|
.update(userTable)
|
|
.set({
|
|
lastLogin: Common.utc(),
|
|
})
|
|
.where(and(eq(userTable.id, id)))
|
|
.execute()
|
|
|
|
),
|
|
)
|
|
|
|
export function serialize(
|
|
input: typeof userTable.$inferSelect
|
|
): z.infer<typeof Info> {
|
|
return {
|
|
id: input.id,
|
|
name: input.name,
|
|
email: input.email,
|
|
avatarUrl: input.avatarUrl,
|
|
lastLogin: input.lastLogin,
|
|
polarCustomerID: input.polarCustomerID,
|
|
}
|
|
}
|
|
} |