monorepo 버전 준비

## @strpc
기존 RPC를 package화

### @strpc/express
express의 middleware + router 결함

## @sammo
게임 전체
- server, client
- gateway_server, gateway_client
This commit is contained in:
2023-09-23 16:29:18 +00:00
parent 734e0cb7bf
commit e59f9a9659
197 changed files with 15458 additions and 3 deletions
@@ -0,0 +1,25 @@
import type { StateIncrementer } from '@sammo/server_util';
import SchemaSequence from './schema/SchemaSequence.js';
import { InvalidArgument } from '@sammo/util';
export function MongoSequenceFactory(collectionName: string): StateIncrementer{
return async (increase: number) => {
if(increase <= 0){
throw new InvalidArgument('increase must be > 0');
}
increase = Math.ceil(increase);
const result = await SchemaSequence.findOneAndUpdate({
collectionName,
}, {
$inc: {
nextSeq: increase,
},
}, {
upsert: true,
new: true,
});
return result.nextSeq;
}
}
@@ -0,0 +1,22 @@
import type { structure } from "@sammo/api_def/gateway";
import { StartSession } from "@sammo/server_util";
import { GET, type APIReturnType } from "@strpc/express";
import { declProcDecorators } from "@strpc/express/proc_decorator";
import { ReqGatewayLogin } from "../procDecorator/ReqGatewayLogin.js";
type BaseAPI = typeof structure.GetGameLoginToken;
type RType = APIReturnType<BaseAPI>;
export const GameLoginTokenSessionKey = "GameLoginToken";
export const GetGameLoginToken = GET<BaseAPI>()(declProcDecorators(
StartSession,
ReqGatewayLogin,
))(
async (query, ctx): RType => {
return {
result: false,
reason: 'NotYetImplemented',
};
}
);
@@ -0,0 +1,18 @@
import type { structure } from "@sammo/api_def/gateway";
import { StartSession } from "@sammo/server_util";
import { GET, type APIReturnType } from "@strpc/express";
import { declProcDecorators } from "@strpc/express/proc_decorator";
import { ReqGatewayLogin } from "../../procDecorator/ReqGatewayLogin.js";
type BaseAPI = typeof structure.Login.ReqNonce;
type RType = APIReturnType<BaseAPI>;
const argValidator = undefined;
export const ReqNonce = GET<BaseAPI>(argValidator)(declProcDecorators(
StartSession,
ReqGatewayLogin,
))(
(query, ctx): RType => {
throw new Error("Method not implemented.");
}
);
@@ -0,0 +1,62 @@
import type { structure } from "@sammo/api_def/gateway";
import { StartSession } from "@sammo/server_util";
import { POST, type APIReturnType } from "@strpc/express";
import { declProcDecorators } from "@strpc/express/proc_decorator";
import { z } from "zod";
import { loginCtxSessionKey, type GatewayLoginCtx } from "@/procDecorator/ReqGatewayLogin.js";
import { delay } from "@sammo/util";
type BaseAPI = typeof structure.Login.LoginByID;
type RType = APIReturnType<BaseAPI>;
const LoginByIDReq = z.object({
id: z.string(),
password: z.string(),
});
export const LoginByID = POST<BaseAPI>(LoginByIDReq)(declProcDecorators(
StartSession,
))(
async (query, ctx, req, res): RType => {
const id = query.id;
const password = query.password;
//TODO: DB에서 뭔가 가져와야 함
await delay(1);
if (Math.random() < 0.3) {
return {
result: false,
reason: "로그인 실패",
reqOTP: false,
}
}
if (Math.random() < 0.5) {
return {
result: false,
reason: "OTP 인증 필요",
reqOTP: true,
}
}
const userID = 1;
const userName = "test";
const userLevel = 1;
const nextToken: [number, string] = [1, "1234567890"];
const loginCtx: GatewayLoginCtx = {
userID,
userName,
userLevel,
allowServerAction: new Map(),
allowGatewayAction: new Set(),
loginDate: new Date(),
}
ctx.session.setItem(loginCtxSessionKey, loginCtx);
return {
result: true,
nextToken,
}
});
@@ -0,0 +1,50 @@
import type { structure } from "@sammo/api_def/gateway";
import { StartSession } from "@sammo/server_util";
import { POST, type APIReturnType } from "@strpc/express";
import { declProcDecorators } from "@strpc/express/proc_decorator";
import { z } from "zod";
import { loginCtxSessionKey, type GatewayLoginCtx } from "@/procDecorator/ReqGatewayLogin.js";
import { delay } from "@sammo/util";
type BaseAPI = typeof structure.Login.LoginByToken;
type RType = APIReturnType<BaseAPI>;
const LoginByTokenReq = z.object({
token_id: z.number(),
hashedToken: z.string(),
});
export const LoginByToken = POST<BaseAPI>(LoginByTokenReq)(declProcDecorators(
StartSession,
))
(async (query, ctx): RType => {
query.hashedToken;
ctx.session.clear();
await delay(1);
//무언가 로그인
//TODO: DB는 어디서 들고옴?
const userID = 1;
const userName = "test";
const userLevel = 1;
const nextToken: [number, string] = [1, "1234567890"];
const loginCtx: GatewayLoginCtx = {
userID,
userName,
userLevel,
allowServerAction: new Map(),
allowGatewayAction: new Set(),
loginDate: new Date(),
}
ctx.session.setItem(loginCtxSessionKey, loginCtx);
//throw new Error("Method not implemented.");
return {
result: true,
nextToken,
}
});
@@ -0,0 +1,30 @@
import type { structure } from "@sammo/api_def/gateway";
import { StartSession } from "@sammo/server_util";
import { GET, type APIReturnType } from "@strpc/express";
import { declProcDecorators } from "@strpc/express/proc_decorator";
type BaseAPI = typeof structure.Login.ReqNonce;
type RType = APIReturnType<BaseAPI>;
export const ReqNonceSessionKey = 'loginNonce';
export const ReqNonce = GET<BaseAPI>(undefined)(declProcDecorators(
StartSession,
))(
async (query, ctx): RType => {
const nonce = ctx.session.getItem<string>(ReqNonceSessionKey);
if (nonce !== undefined) {
return {
loginNonce: nonce,
result: true,
}
}
const newNonce = "1234567890";
ctx.session.setItem(ReqNonceSessionKey, newNonce);
return {
loginNonce: newNonce,
result: true,
}
}
)
@@ -0,0 +1,11 @@
import type { structure } from "@sammo/api_def/gateway";
import type { APINamespaceType } from "@strpc/express";
import { LoginByID } from "./LoginByID.js";
import { LoginByToken } from "./LoginByToken.js";
import { ReqNonce } from "./ReqNonce.js";
export const Login = {
LoginByID,
LoginByToken,
ReqNonce,
} satisfies APINamespaceType<typeof structure.Login>;
+9
View File
@@ -0,0 +1,9 @@
import type { structure } from "@sammo/api_def/gateway";
import { GetGameLoginToken } from "./GetGameLoginToken.js";
import { Login } from "./Login/index.js";
import type { APINamespaceType } from "@strpc/express";
export const sammoGatewayAPI = {
Login,
GetGameLoginToken
} satisfies APINamespaceType<typeof structure>;
+22
View File
@@ -0,0 +1,22 @@
import './dotenv.js';
import { connect, Mongoose } from "mongoose";
import { unwrap } from '@sammo/util';
const dbConfig = {
host: unwrap(process.env.GATEWAY_DB_HOST),
port: Number(unwrap(process.env.GATEWAY_DB_PORT)),
user: unwrap(process.env.GATEWAY_DB_USER),
password: unwrap(process.env.GATEWAY_DB_PASSWORD),
database: unwrap(process.env.GATEWAY_DB_DATABASE),
}
const db: Promise<Mongoose> = (async () => {
return await connect(`mongodb://${dbConfig.host}:${dbConfig.port}/${dbConfig.database}`, {
auth:{
username: dbConfig.user,
password: dbConfig.password,
}
});
})();
export default db;
+3
View File
@@ -0,0 +1,3 @@
import { resolve } from "node:path";
export const rootPath = resolve(resolve(), '../..');
+47
View File
@@ -0,0 +1,47 @@
import dotenv from 'dotenv';
import { rootPath } from './constPath.js';
import { unwrap } from '@sammo/util';
let init = false;
const dirPath = rootPath
console.log(rootPath);
function initDotEnv() {
if (process.env['NODE_ENV'] == 'production') {
dotenv.config({ path: `${dirPath}/.env.production.local` });
dotenv.config({ path: `${dirPath}/.env.local` });
dotenv.config({ path: `${dirPath}/.env.production` });
}
else {
dotenv.config({ path: `${dirPath}/.env.development.local` });
dotenv.config({ path: `${dirPath}/.env.local` });
dotenv.config({ path: `${dirPath}/.env.development` });
}
dotenv.config();
init = true;
}
if (!init) {
initDotEnv();
}
let _ownConfig: ReturnType<typeof generateConfig>|undefined = undefined;
function generateConfig() {
return {
port: parseInt(process.env['SERVER_PORT'] ?? "3001"),
sessionSecret: unwrap(process.env['SESSION_SECRET']),
apiRootPath: process.env['API_ROOT_PATH'] ?? '/api',
} as const;
}
export function ownConfig() {
if(_ownConfig !== undefined){
return _ownConfig;
}
if(!init){
initDotEnv();
}
_ownConfig = generateConfig();
return _ownConfig;
}
+9
View File
@@ -0,0 +1,9 @@
export type {
ServerActionType,
UserIDType,
IUser,
} from "./schema/User.js";
export type {
ILoginToken
} from "./schema/LoginToken.js";
+34
View File
@@ -0,0 +1,34 @@
import './dotenv.js'
import { ownConfig } from './dotenv.js';
import 'reflect-metadata';
import gatewayDB from './connectDB.js';
import express, { type Request, type Response } from "express"
import session from "express-session";
import { buildAPISystem } from '@strpc/express/generator';
import { sammoGatewayAPI } from './api/index.js';
import { unwrap } from '@sammo/util';
const gatewayConfig = ownConfig();
gatewayDB.then(async (gatewayDB) => {
// create express app
const app = express()
app.use(express.json());
app.use(session({
secret: unwrap(gatewayConfig.sessionSecret),
resave: false,
saveUninitialized: false,
}))
//app.set('etag', false);
app.use('/gateway_api', buildAPISystem(sammoGatewayAPI));
// start express server
app.listen(gatewayConfig.port)
console.log(`Gateway server has started on port ${gatewayConfig.port}`)
}).catch(error => console.log(error));
export default {};
@@ -0,0 +1,33 @@
import type { SessionCtx } from "@sammo/server_util";
import type { GatewayActionType, ServerActionType } from "../schema/User.js";
import type { ProcDecorator } from "@strpc/express/proc_decorator";
export type GatewayLoginCtx = {
userID: number;
userName: string;
userLevel: number;
allowServerAction: Map<string, Set<ServerActionType>>;
allowGatewayAction: Set<GatewayActionType>;
loginDate: Date;
}
export const loginCtxSessionKey = 'loginCtx';
export function ReqGatewayLogin<Q extends SessionCtx>(): ProcDecorator<GatewayLoginCtx & Q, Q> {
return (ctx) => {
const loginCtx = ctx.session.getItem<GatewayLoginCtx>(loginCtxSessionKey);
if(!loginCtx){
return [{
result: false,
type: 'Required Login',
info: 'ReqLogin'
}, ctx];
}
return [{
result: true,
},{
...loginCtx,
...ctx,
}];
}
}
@@ -0,0 +1,23 @@
import { type UserIDType } from "./User.js";
import { Mongoose, Schema, model } from "mongoose";
export interface ILoginToken {
userID: UserIDType;
token: string;
ip: string;
regDate: Date;
validUntil: Date;
}
export const LoginToken = new Schema<ILoginToken>({
userID: { type: Number, required: true },
token: { type: String, required: true },
ip: { type: String, required: true },
regDate: { type: Date, required: true },
validUntil: { type: Date, required: true },
}, { autoIndex: false, autoCreate: false, })
.index({ userID: 1, token: 1 })
.index({ validUntil: 1 }, { expireAfterSeconds: 1 })
;
export default (conn: Mongoose)=>conn.model<ILoginToken>('LoginToken', LoginToken);
@@ -0,0 +1,23 @@
import { Schema, model, Types } from 'mongoose';
interface ISchemaSequence {
_id: Types.ObjectId;
collectionName: string;
nextSeq: number;
}
export const SchemaSequence = new Schema<ISchemaSequence>({
collectionName: { type: String, required: true },
nextSeq: {
type: Number, required: true,
get: (v: number) => Math.ceil(v),
set: (v: number) => Math.ceil(v),
default: 0,
},
}, { autoIndex: true, autoCreate: true })
.index({ collectionName: 1 }, { unique: true })
;
export default model<ISchemaSequence>('SchemaSequence', SchemaSequence);
@@ -0,0 +1,19 @@
import { Schema, model } from "mongoose";
export interface IServerVersion {
serverKey: string;
branch: string;
version: string;
updateDate: Date;
}
export const ServerVersion = new Schema<IServerVersion>({
serverKey: { type: String, required: true },
branch: { type: String, required: true },
version: { type: String, required: true },
updateDate: { type: Date, required: true },
}, { autoIndex: false, autoCreate: false, })
.index({ serverKey: 1 }, { unique: true })
;
export default model<IServerVersion>('ServerVersion', ServerVersion);
+90
View File
@@ -0,0 +1,90 @@
import { Schema, model } from 'mongoose';
export type UserIDType = number;
const validOAuthTypeList = ['KAKAO', 'NONE'] as const;
export type validOAuthType = typeof validOAuthTypeList[number];
const validServerActionTypeList = [
'Update', 'UpdateByGitPath', 'ShowErrorLog',
'CloseServer', 'OpenServer', 'StopAndResumeServer', 'OpenVote',
] as const;
export type ServerActionType = typeof validServerActionTypeList[number];
const validGatewayActionTypeList = [
'Update', 'UpdateByGitPath', 'ShowErrorLog', 'ResetUserPassword', 'ChangeGatewayState', 'DeleteUser',
] as const;
export type GatewayActionType = typeof validGatewayActionTypeList[number];
export interface IUser {
_id: UserIDType;
oauthID?: bigint;
id: string;
email: string;
oauthType: validOAuthType;
oauthInfo?: object;
tokenValidUntil?: Date;
userSalt: string;
hashedPassword: string;
allowThirdPartyUse: boolean;
userName: string;
allowServerAction?: Map<string, ServerActionType[]>;
allowGatewayAction?: ServerActionType[];
penalty?: Map<string, Date>;
picture?: string;
useImgSvr?: boolean;
regDate: Date;
deleteAfter?: Date;
}
export const User = new Schema<IUser>({
_id: { type: Number, required: true },
oauthID: { type: BigInt, required: false },
id: { type: String, required: true },
email: { type: String, required: true },
oauthType: { type: String, required: true, enum: validOAuthTypeList },
oauthInfo: { type: Object, required: false },
tokenValidUntil: { type: Date, required: false },
userSalt: { type: String, required: true },
hashedPassword: { type: String, required: true },
allowThirdPartyUse: { type: Boolean, required: true },
userName: { type: String, required: true },
allowServerAction: {
type: Map, required: false, of: {
type: Array,
of: { type: String, enum: validServerActionTypeList }
}
},
allowGatewayAction: {
type: Array, required: false, of: {
type: String, enum: validGatewayActionTypeList
}
},
penalty: { type: Map, required: false, of: Date },
picture: { type: String, required: false },
useImgSvr: { type: Boolean, required: false },
regDate: { type: Date, required: true },
deleteAfter: { type: Date, required: false },
}, { autoIndex: false, autoCreate: false })
.index({ id: 1 }, { unique: true })
.index({ email: 1 }, { unique: true })
.index({ oauthID: 1 }, { unique: true, sparse: true })
.index({ deleteAfter: 1 }) // 자동 삭제 아님!
;
export default model<IUser>('User', User);
@@ -0,0 +1,30 @@
import { Schema, model, } from 'mongoose';
import { type UserIDType } from './User.js';
export type LogType = 'register'
| 'login_pw' | 'login_token' | 'login_oauth' | 'logout'
| 'change_pw' | 'reset_pw';
const LogTypeList: LogType[] = [
'register',
'login_pw', 'login_token', 'login_oauth', 'logout',
'change_pw', 'reset_pw',
];
interface IUserLog {
userID: UserIDType;
logDate: Date;
logType: LogType;
action: object;
}
export const UserLog = new Schema<IUserLog>({
userID: { type: Number, required: true },
logDate: { type: Date, required: true },
logType: { type: String, required: true, enum: LogTypeList },
action: { type: Object, required: true },
}, { autoIndex: false, autoCreate: false, })
.index({ userID: 1, logDate: 1 })
.index({ logDate: 1 }, { expireAfterSeconds: 60 * 60 * 24 * 365 * 3 })
;
export default model<IUserLog>('UserLog', UserLog);