♻️ Splitting up services (skip ci)

This commit is contained in:
2025-01-10 00:18:26 +08:00
parent df6679bbe3
commit c1140b4e2f
36 changed files with 393 additions and 148 deletions

175
packages/sn/.gitignore vendored Normal file
View File

@@ -0,0 +1,175 @@
# Based on https://raw.githubusercontent.com/github/gitignore/main/Node.gitignore
# Logs
logs
_.log
npm-debug.log_
yarn-debug.log*
yarn-error.log*
lerna-debug.log*
.pnpm-debug.log*
# Caches
.cache
# Diagnostic reports (https://nodejs.org/api/report.html)
report.[0-9]_.[0-9]_.[0-9]_.[0-9]_.json
# Runtime data
pids
_.pid
_.seed
*.pid.lock
# Directory for instrumented libs generated by jscoverage/JSCover
lib-cov
# Coverage directory used by tools like istanbul
coverage
*.lcov
# nyc test coverage
.nyc_output
# Grunt intermediate storage (https://gruntjs.com/creating-plugins#storing-task-files)
.grunt
# Bower dependency directory (https://bower.io/)
bower_components
# node-waf configuration
.lock-wscript
# Compiled binary addons (https://nodejs.org/api/addons.html)
build/Release
# Dependency directories
node_modules/
jspm_packages/
# Snowpack dependency directory (https://snowpack.dev/)
web_modules/
# TypeScript cache
*.tsbuildinfo
# Optional npm cache directory
.npm
# Optional eslint cache
.eslintcache
# Optional stylelint cache
.stylelintcache
# Microbundle cache
.rpt2_cache/
.rts2_cache_cjs/
.rts2_cache_es/
.rts2_cache_umd/
# Optional REPL history
.node_repl_history
# Output of 'npm pack'
*.tgz
# Yarn Integrity file
.yarn-integrity
# dotenv environment variable files
.env
.env.development.local
.env.test.local
.env.production.local
.env.local
# parcel-bundler cache (https://parceljs.org/)
.parcel-cache
# Next.js build output
.next
out
# Nuxt.js build / generate output
.nuxt
dist
# Gatsby files
# Comment in the public line in if your project uses Gatsby and not Next.js
# https://nextjs.org/blog/next-9-1#public-directory-support
# public
# vuepress build output
.vuepress/dist
# vuepress v2.x temp and cache directory
.temp
# Docusaurus cache and generated files
.docusaurus
# Serverless directories
.serverless/
# FuseBox cache
.fusebox/
# DynamoDB Local files
.dynamodb/
# TernJS port file
.tern-port
# Stores VSCode versions used for testing VSCode extensions
.vscode-test
# yarn v2
.yarn/cache
.yarn/unplugged
.yarn/build-state.yml
.yarn/install-state.gz
.pnp.*
# IntelliJ based IDEs
.idea
# Finder (MacOS) folder config
.DS_Store

BIN
packages/sn/bun.lockb Executable file

Binary file not shown.

32
packages/sn/package.json Normal file
View File

@@ -0,0 +1,32 @@
{
"name": "solar-js-sdk",
"main": "dist/index.js",
"types": "dist/index.d.ts",
"type": "module",
"tsup": {
"entry": [
"src/index.ts"
],
"splitting": true,
"sourcemap": true,
"clean": true,
"dts": true,
"format": "esm"
},
"scripts": {
"build": "tsup"
},
"devDependencies": {
"@types/bun": "latest",
"tsup": "^8.3.5"
},
"peerDependencies": {
"typescript": "^5.0.0"
},
"dependencies": {
"axios": "^1.7.9",
"axios-case-converter": "^1.1.1",
"universal-cookie": "^7.2.2",
"zustand": "^5.0.3"
}
}

View File

@@ -0,0 +1,48 @@
import { sni } from './network'
export interface SnAttachment {
id: number
createdAt: Date
updatedAt: Date
deletedAt?: Date | null
rid: string
uuid: string
size: number
name: string
alt: string
mimetype: string
hash: string
destination: number
refCount: number
contentRating: number
qualityRating: number
cleanedAt?: Date | null
isAnalyzed: boolean
isSelfRef: boolean
isIndexable: boolean
ref?: SnAttachment | null
refId?: number | null
poolId?: number | null
accountId: number
thumbnailId?: number | null
thumbnail?: SnAttachment | null
compressedId?: number | null
compressed?: SnAttachment | null
usermeta: Record<string, any>
metadata: Record<string, any>
}
export async function getAttachment(id: string | number): Promise<SnAttachment> {
const resp = await sni.get<SnAttachment>('/cgi/uc/attachments/' + id + '/meta')
return resp.data
}
export async function listAttachment(id: string[]): Promise<SnAttachment[]> {
const resp = await sni.get<{ data: SnAttachment[] }>('/cgi/uc/attachments', {
params: {
id: id.join(','),
take: id.length,
},
})
return resp.data.data
}

57
packages/sn/src/auth.ts Normal file
View File

@@ -0,0 +1,57 @@
import Cookies from 'universal-cookie'
export interface SnAuthResult {
isFinished: boolean
ticket: SnAuthTicket
}
export interface SnAuthTicket {
id: number
createdAt: Date
updatedAt: Date
deletedAt?: Date | null
stepRemain: number
grantToken?: string | null
accessToken?: string | null
refreshToken?: string | null
ipAddress: string
location: string
userAgent: string
expiredAt?: Date | null
lastGrantAt?: Date | null
availableAt?: Date | null
nonce?: string | null
accountId?: number | null
factorTrail: number[]
}
export interface SnAuthFactor {
id: number
createdAt: Date
updatedAt: Date
deletedAt?: Date | null
type: number
config?: Record<string, any> | null
accountId?: number | null
}
export function setTokenCookies(atk: string, rtk: string) {
const cookies = new Cookies()
cookies.set('nex_user_atk', atk, { path: '/', maxAge: 2592000 })
cookies.set('nex_user_rtk', rtk, { path: '/', maxAge: 2592000 })
}
export function removeTokenCookies() {
const cookies = new Cookies()
cookies.remove('nex_user_atk')
cookies.remove('nex_user_rtk')
}
export function checkAuthenticatedClient(): boolean {
const cookies = new Cookies()
return !!cookies.get('nex_user_atk')
}
export function redirectToLogin() {
window.open('/auth/login?redirect_uri=' + encodeURIComponent(window.location.pathname), '_self')
}

View File

@@ -0,0 +1,10 @@
export interface SnCheckInRecord {
id: number
createdAt: Date
updatedAt: Date
deletedAt?: Date | null
resultTier: number
resultExperience: number
resultModifiers: number[]
accountId: number
}

7
packages/sn/src/index.ts Normal file
View File

@@ -0,0 +1,7 @@
export * from './matrix/product'
export * from './attachment'
export * from './auth'
export * from './checkIn'
export * from './network'
export * from './post'
export * from './user'

View File

@@ -0,0 +1,25 @@
export interface MaProduct {
id: number
created_at: Date
updated_at: Date
deleted_at?: Date
icon: string
name: string
alias: string
description: string
previews: string[]
tags: string[]
meta: MaProductMeta
releases: null
account_id: number
}
export interface MaProductMeta {
id: number
created_at: Date
updated_at: Date
deleted_at?: Date
introduction: string
attachments: string[]
product_id: number
}

View File

@@ -0,0 +1,78 @@
import axios, { type AxiosInstance } from 'axios'
import applyCaseMiddleware from 'axios-case-converter'
import Cookies from 'universal-cookie'
import { setTokenCookies } from './auth'
const baseURL = 'https://api.sn.solsynth.dev'
export const sni: AxiosInstance = (() => {
const inst = axios.create({
baseURL,
})
inst.interceptors.request.use(
async (config) => {
const tk = await refreshToken()
if (tk) config.headers['Authorization'] = `Bearer ${tk}`
return config
},
(error) => error,
)
applyCaseMiddleware(inst, {
ignoreParams: true,
ignoreHeaders: true,
})
return inst
})()
async function refreshToken(): Promise<string | undefined> {
const cookies = new Cookies()
if (!cookies.get('nex_user_atk') || !cookies.get('nex_user_rtk')) return
const ogTk: string = cookies.get('nex_user_atk')!
if (!isTokenExpired(ogTk)) return ogTk
const resp = await axios.post(
'/cgi/id/auth/token',
{
refresh_token: cookies.get('nex_user_rtk')!,
grant_type: 'refresh_token',
},
{ baseURL },
)
const atk: string = resp.data['access_token']
const rtk: string = resp.data['refresh_token']
setTokenCookies(atk, rtk)
console.log('[Authenticator] Refreshed token...')
return atk
}
function isTokenExpired(token: string): boolean {
try {
const parts = token.split('.')
if (parts.length !== 3) {
throw new Error('Invalid JWT format')
}
const payload = JSON.parse(atob(parts[1]))
if (!payload.exp) {
throw new Error("'exp' claim is missing in the JWT payload")
}
const now = Math.floor(Date.now() / 1000)
return now >= payload.exp
} catch (error) {
console.error('[Authenticator] Something went wrong with token: ', error)
return true
}
}
export function getAttachmentUrl(identifer: string): string {
if (identifer.startsWith('http')) return identifer
return `${baseURL}/cgi/uc/attachments/${identifer}`
}

85
packages/sn/src/post.ts Normal file
View File

@@ -0,0 +1,85 @@
export interface SnPost {
id: number
createdAt: Date
updatedAt: Date
deletedAt?: Date | null
type: string
body: SnPostBody & Record<string, any>
language: string
alias?: string | null
aliasPrefix?: string | null
tags: SnPostTag[]
categories: SnPostCategory[]
replies?: SnPost[] | null
replyId?: number | null
repostId?: number | null
replyTo?: SnPost | null
repostTo?: SnPost | null
visibleUsersList?: number[] | null
invisibleUsersList?: number[] | null
visibility: number
editedAt?: Date | null
pinnedAt?: Date | null
lockedAt?: Date | null
isDraft: boolean
publishedAt?: Date | null
publishedUntil?: Date | null
totalUpvote: number
totalDownvote: number
publisherId: number
publisher: SnPublisher
metric: SnMetric
}
export interface SnPostTag {
id: number
createdAt: Date
updatedAt: Date
deletedAt?: Date
alias: string
name: string
description: string
posts?: SnPost[]
}
export interface SnPostCategory {
id: number
createdAt: Date
updatedAt: Date
deletedAt?: Date
alias: string
name: string
description: string
posts?: SnPost[]
}
export interface SnPostBody {
attachments: string[]
content: string
location?: string
thumbnail?: string
title?: string
}
export interface SnMetric {
replyCount: number
reactionCount: number
reactionList: Record<string, number>
}
export interface SnPublisher {
id: number
createdAt: Date
updatedAt: Date
deletedAt?: Date | null
type: number
name: string
nick: string
description: string
avatar: string
banner: string
totalUpvote: number
totalDownvote: number
realmId?: number | null
accountId: number
}

83
packages/sn/src/user.ts Normal file
View File

@@ -0,0 +1,83 @@
import { create } from 'zustand'
import { sni } from './network'
import Cookies from 'universal-cookie'
export interface SnAccount {
id: number
createdAt: Date
updatedAt: Date
deletedAt?: Date | null
confirmedAt?: Date | null
contacts?: SnAccountContact[] | null
avatar: string
banner: string
description: string
name: string
nick: string
permNodes: Record<string, any>
profile?: SnAccountProfile | null
badges: SnAccountBadge[]
suspendedAt?: Date | null
affiliatedId?: number | null
affiliatedTo?: number | null
automatedBy?: number | null
automatedId?: number | null
}
export interface SnAccountContact {
accountId: number
content: string
createdAt: Date
deletedAt?: Date | null
id: number
isPrimary: boolean
isPublic: boolean
type: number
updatedAt: Date
verifiedAt?: Date | null
}
export interface SnAccountProfile {
id: number
accountId: number
birthday?: Date | null
createdAt: Date
deletedAt?: Date | null
experience: number
firstName: string
lastName: string
lastSeenAt?: Date | null
updatedAt: Date
}
export interface SnAccountBadge {
id: number
createdAt: Date
updatedAt: Date
deletedAt?: Date | null
type: string
accountId: number
metadata: Record<string, any>
}
export interface UserStore {
account: SnAccount | undefined
fetchUser: () => Promise<SnAccount | undefined>
}
export const useUserStore = create<UserStore>((set) => ({
account: undefined,
fetchUser: async (): Promise<SnAccount | undefined> => {
const cookies = new Cookies()
if (!cookies.get('nex_user_atk')) return
try {
const resp = await sni.get<SnAccount>('/cgi/id/users/me')
set({ account: resp.data })
console.log('[Authenticator] Logged in as @' + resp.data.name)
return resp.data
} catch (err) {
console.error('[Authenticator] Unable to get user profile: ', err)
return
}
},
}))

27
packages/sn/tsconfig.json Normal file
View File

@@ -0,0 +1,27 @@
{
"compilerOptions": {
// Enable latest features
"lib": ["ESNext", "DOM"],
"target": "ESNext",
"module": "ESNext",
"moduleDetection": "force",
"jsx": "react-jsx",
"allowJs": true,
// Bundler mode
"moduleResolution": "bundler",
"allowImportingTsExtensions": true,
"verbatimModuleSyntax": true,
"noEmit": true,
// Best practices
"strict": true,
"skipLibCheck": true,
"noFallthroughCasesInSwitch": true,
// Some stricter flags (disabled by default)
"noUnusedLocals": false,
"noUnusedParameters": false,
"noPropertyAccessFromIndexSignature": false
}
}