Skip to content

Commit

Permalink
Add server authentification
Browse files Browse the repository at this point in the history
  • Loading branch information
ai committed Aug 29, 2024
1 parent 775ecb6 commit e7d2b1f
Show file tree
Hide file tree
Showing 20 changed files with 744 additions and 109 deletions.
1 change: 1 addition & 0 deletions .devcontainer/Dockerfile
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@ ENV PNPM_CHECKSUM_X64 445615d1b6fff3d6999bcc9ee385bc543e35181b9814631c8481b76933

RUN apt-get update \
&& apt-get install -y eza zsh git tig ripgrep bat curl tar micro psmisc \
build-essential python3 \
&& apt-get clean \
&& rm -rf /var/lib/apt/lists/*

Expand Down
2 changes: 2 additions & 0 deletions .github/workflows/main.yml
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,8 @@ jobs:
uses: actions/checkout@v4
- name: Initialize Node.js
uses: ./.github/actions/init-node
- name: Compile dependencies
run: pnpm run --filter server prepare:bcrypt
- name: Run tests
run: pnpm run -r --include-workspace-root '/^test:(?!markdown\b|proxy-coverage\b|loaders\b)/'
env:
Expand Down
41 changes: 41 additions & 0 deletions api/http/signin.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
import {
type Endpoint,
fetchJSON,
type FetchOptions,
hasStringKey
} from './utils.ts'

export interface SignInRequest {
password: string
userId: string
}

export interface SignInResponse {
session: string
}

const METHOD = 'POST'

export async function signIn(
params: SignInRequest,
opts?: FetchOptions
): Promise<SignInResponse> {
return fetchJSON(METHOD, '/session', params, opts)
}

export const signInEndpoint: Endpoint<SignInResponse, SignInRequest> = {
checkBody(body) {
if (hasStringKey(body, 'userId') && hasStringKey(body, 'password')) {
return body
}
return false
},
method: METHOD,
parseUrl(url) {
if (url === '/session') {
return {}
} else {
return false
}
}
}
39 changes: 39 additions & 0 deletions api/http/signout.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
import {
type Endpoint,
fetchJSON,
type FetchOptions,
hasStringKey,
isEmptyObject
} from './utils.ts'

export type SignOutRequest = {
session?: string
}

export type SignOutResponse = {}

const METHOD = 'DELETE'

export async function signOut(
params: SignOutRequest,
opts?: FetchOptions
): Promise<SignOutResponse> {
return fetchJSON(METHOD, '/session', params, opts)
}

export const signOutEndpoint: Endpoint<SignOutResponse, SignOutRequest> = {
checkBody(body) {
if (isEmptyObject(body) || hasStringKey(body, 'session')) {
return body
}
return false
},
method: METHOD,
parseUrl(url) {
if (url === '/session') {
return {}
} else {
return false
}
}
}
51 changes: 51 additions & 0 deletions api/http/signup.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,51 @@
import {
type Endpoint,
fetchJSON,
type FetchOptions,
hasStringKey
} from './utils.ts'

export interface SignUpRequest {
id: string
password: string
}

export interface SignUpResponse {
id: string
session: string
}

const METHOD = 'PUT'

export async function signUp(
params: SignUpRequest,
opts?: FetchOptions
): Promise<SignUpResponse> {
return fetchJSON(METHOD, `/users/${params.id}`, params, opts)
}

const URL_PATTERN = /^\/users\/([^/]+)$/

export const signUpEndpoint: Endpoint<
SignUpResponse,
SignUpRequest,
{ id: string }
> = {
checkBody(body, urlParams) {
if (hasStringKey(body, 'id') && hasStringKey(body, 'password')) {
if (body.id === urlParams.id) {
return body
}
}
return false
},
method: METHOD,
parseUrl(url) {
let match = url.match(URL_PATTERN)
if (match) {
return { id: match[1]! }
} else {
return false
}
}
}
61 changes: 61 additions & 0 deletions api/http/utils.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,61 @@
export function isObject(body: unknown): body is object {
return typeof body === 'object' && body !== null
}

export function isEmptyObject(body: unknown): body is Record<never, never> {
return isObject(body) && Object.keys(body).length === 0
}

export function hasKey<Key extends string>(
body: unknown,
key: Key
): body is Record<Key, unknown> {
return isObject(body) && key in body
}

export function hasStringKey<Key extends string>(
body: unknown,
key: Key
): body is Record<Key, string> {
return hasKey(body, key) && typeof body[key] === 'string'
}

export interface FetchOptions {
fetch?: typeof fetch
host?: string
response?: (res: Response) => void
}

export async function fetchJSON<Response = unknown>(
method: string,
url: string,
body: object,
opts: FetchOptions | undefined = {}
): Promise<Response> {
let host = opts.host ?? ''
let request = opts.fetch ?? fetch
let response = await request(host + url, {
body: JSON.stringify(body),
headers: {
'Content-Type': 'application/json'
},
method
})
if (!response.ok) {
throw new Error(await response.text())
}
if (opts.response) opts.response(response)
return response.json()
}

export interface Endpoint<
// Need to put it inside type to pass all types to server in a single variable
// eslint-disable-next-line @typescript-eslint/no-unused-vars
Response,
Request,
UrlParams extends Record<string, string> = Record<never, string>
> {
checkBody(body: unknown, urlParams: UrlParams): false | Request
method: string
parseUrl(url: string): false | UrlParams
}
6 changes: 6 additions & 0 deletions api/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,3 +2,9 @@
* Client’s protocol version
*/
export const SUBPROTOCOL = '0.0.0'

export * from './http/signin.ts'
export * from './http/signout.ts'
export * from './http/signup.ts'
export type { Endpoint } from './http/utils.ts'
export * from './password.ts'
3 changes: 3 additions & 0 deletions api/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -9,5 +9,8 @@
"exports": {
".": "./index.ts",
"./package.json": "./package.json"
},
"dependencies": {
"@logux/actions": "0.4.0"
}
}
16 changes: 16 additions & 0 deletions api/password.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
import { defineAction } from '@logux/actions'

export interface SetPasswordAction {
password: string
type: 'passwords/set'
userId?: string
}

export const setPassword = defineAction<SetPasswordAction>('passwords/set')

export interface DeletePasswordAction {
type: 'passwords/delete'
}

export const deletePassword =
defineAction<DeletePasswordAction>('passwords/delete')
Loading

0 comments on commit e7d2b1f

Please sign in to comment.