-
Notifications
You must be signed in to change notification settings - Fork 1
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
feat(passwords): web user creation and password update
- Loading branch information
Showing
8 changed files
with
575 additions
and
2 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,22 @@ | ||
import * as t from "io-ts" | ||
|
||
import { RadiusUserPasswordStatusType, RadiusUserPasswords, RadiusUserType } from "../types/users/RadiusUser" | ||
|
||
export const CreateOrUpdateUserRequestType = t.partial({}) | ||
|
||
export type CreateOrUpdateUserRequest = t.TypeOf<typeof CreateOrUpdateUserRequestType> | ||
|
||
export const ListUserResponseType = t.readonlyArray(RadiusUserType) | ||
|
||
export type ListUserResponse = t.TypeOf<typeof ListUserResponseType> | ||
|
||
export const ListUserPasswordStatusResponseType = t.readonlyArray(RadiusUserPasswordStatusType) | ||
|
||
export const UpdateUserPasswordsRequestType: t.Type<Partial<RadiusUserPasswords>> = t.partial({ | ||
clearText: t.union([t.string, t.null]), | ||
ntHash: t.union([t.string, t.null]), | ||
ssha: t.union([t.string, t.null]), | ||
ssha512: t.union([t.string, t.null]), | ||
}) | ||
|
||
export type UpdateUserPasswordsRequest = t.TypeOf<typeof UpdateUserPasswordsRequestType> |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,108 @@ | ||
import { | ||
BadRequestException, | ||
Body, | ||
Controller, | ||
Delete, | ||
Get, | ||
Inject, | ||
Param, | ||
Post, | ||
UseInterceptors, | ||
} from "@nestjs/common" | ||
import { | ||
CreateOrUpdateUserRequestType, | ||
ListUserPasswordStatusResponseType, | ||
ListUserResponse, | ||
UpdateUserPasswordsRequestType, | ||
} from "@yonagi/common/api/users" | ||
import { RadiusUserPasswordStatus } from "@yonagi/common/types/users/RadiusUser" | ||
import { UsernameType } from "@yonagi/common/types/users/Username" | ||
import { mapValidationLeftError } from "@yonagi/common/utils/Either" | ||
import { getOrThrow, tryCatchF } from "@yonagi/common/utils/TaskEither" | ||
import * as E from "fp-ts/lib/Either" | ||
import * as TE from "fp-ts/lib/TaskEither" | ||
import * as F from "fp-ts/lib/function" | ||
|
||
import { ResponseInterceptor } from "./api.middleware" | ||
import { EncodeResponseWith } from "./common" | ||
import { AbstractRadiusUserPasswordStorage, AbstractRadiusUserStorage } from "../storages" | ||
|
||
@Controller("/api/v1/users") | ||
@UseInterceptors(ResponseInterceptor) | ||
export class RadiusUserController { | ||
@Get("/") | ||
async all(): Promise<ListUserResponse> { | ||
return await this.storage.all() | ||
} | ||
|
||
@Post("/:username") | ||
async createOrUpdate(@Param("username") unknownUsername: unknown, @Body() u: unknown): Promise<void> { | ||
await F.pipe( | ||
TE.fromEither( | ||
F.pipe( | ||
E.Do, | ||
E.bindW("username", () => UsernameType.decode(unknownUsername)), | ||
E.bindW("form", () => CreateOrUpdateUserRequestType.decode(u)), | ||
mapValidationLeftError((e) => new BadRequestException(String(e))), | ||
), | ||
), | ||
tryCatchF( | ||
({ username, form }) => this.storage.createOrUpdate(username, { username, ...form }), | ||
(reason) => new Error(String(reason)), | ||
), | ||
getOrThrow(), | ||
)() | ||
} | ||
|
||
@Delete("/:username") | ||
async delete(@Param("username") unknownUsername: unknown): Promise<void> { | ||
await F.pipe( | ||
TE.fromEither( | ||
F.pipe( | ||
UsernameType.decode(unknownUsername), | ||
mapValidationLeftError((e) => new Error(String(e))), | ||
), | ||
), | ||
tryCatchF( | ||
(username) => this.storage.deleteByUsername(username), | ||
(reason) => new Error(String(reason)), | ||
), | ||
getOrThrow(), | ||
)() | ||
} | ||
|
||
constructor(@Inject(AbstractRadiusUserStorage) private readonly storage: AbstractRadiusUserStorage) {} | ||
} | ||
|
||
@Controller("/api/v1/passwords") | ||
@UseInterceptors(ResponseInterceptor) | ||
export class RadiusUserPasswordController { | ||
@Get("/") | ||
@EncodeResponseWith(ListUserPasswordStatusResponseType) | ||
async all(): Promise<readonly RadiusUserPasswordStatus[]> { | ||
return await this.storage.allStatus() | ||
} | ||
|
||
@Post("/:username") | ||
async update(@Param("username") unknownUsername: unknown, @Body() u: unknown): Promise<void> { | ||
await F.pipe( | ||
TE.fromEither( | ||
F.pipe( | ||
E.Do, | ||
E.bindW("username", () => UsernameType.decode(unknownUsername)), | ||
E.bindW("form", () => UpdateUserPasswordsRequestType.decode(u)), | ||
mapValidationLeftError((e) => new BadRequestException(String(e))), | ||
), | ||
), | ||
tryCatchF( | ||
({ username, form }) => this.storage.createOrUpdate(username, form), | ||
(reason) => new Error(String(reason)), | ||
), | ||
getOrThrow(), | ||
)() | ||
} | ||
|
||
constructor( | ||
@Inject(AbstractRadiusUserPasswordStorage) private readonly storage: AbstractRadiusUserPasswordStorage, | ||
) {} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,38 @@ | ||
"use server" | ||
|
||
import { | ||
CreateOrUpdateUserRequestType, | ||
ListUserPasswordStatusResponseType, | ||
ListUserResponse, | ||
ListUserResponseType, | ||
UpdateUserPasswordsRequest, | ||
UpdateUserPasswordsRequestType, | ||
} from "@yonagi/common/api/users" | ||
import { RadiusUserPasswordStatus } from "@yonagi/common/types/users/RadiusUser" | ||
import { Username } from "@yonagi/common/types/users/Username" | ||
import * as t from "io-ts" | ||
|
||
import { deleteEndpoint, getTypedEndpoint, postTypedEndpoint } from "../../lib/actions" | ||
|
||
export async function createUser(username: string): Promise<void> { | ||
await postTypedEndpoint(t.unknown, CreateOrUpdateUserRequestType, `api/v1/users/${username}`, {}) | ||
} | ||
|
||
export async function deleteUser(username: string): Promise<void> { | ||
await deleteEndpoint(`api/v1/users/${username}`) | ||
} | ||
|
||
export async function listUsers(): Promise<ListUserResponse> { | ||
return await getTypedEndpoint(ListUserResponseType, "api/v1/users") | ||
} | ||
|
||
export async function listUserPasswords(): Promise<readonly RadiusUserPasswordStatus[]> { | ||
return await getTypedEndpoint(ListUserPasswordStatusResponseType, "api/v1/passwords") | ||
} | ||
|
||
export async function updateUserPasswords(username: Username, passwords: UpdateUserPasswordsRequest): Promise<void> { | ||
await postTypedEndpoint(t.unknown, UpdateUserPasswordsRequestType, `api/v1/passwords/${username}`, { | ||
...passwords, | ||
username, | ||
}) | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,26 @@ | ||
"use client" | ||
|
||
import { md4, sha512 } from "hash-wasm" | ||
|
||
export async function ssha512(password: string): Promise<string> { | ||
const data = new TextEncoder().encode(password) | ||
const salt = crypto.getRandomValues(new Uint8Array(16)) | ||
|
||
const hash = await sha512(new Uint8Array([...data, ...salt])) | ||
return hash + Array.prototype.map.call(salt, (byte: number) => byte.toString(16).padStart(2, "0")).join("") | ||
} | ||
|
||
export async function nthash(password: string): Promise<string> { | ||
const utf16 = new Uint16Array(password.length) | ||
for (let i = 0; i < password.length; i++) { | ||
utf16[i] = password.charCodeAt(i) | ||
} | ||
|
||
const utf16le = new Uint8Array(utf16.length * 2) | ||
for (let i = 0; i < utf16.length; i++) { | ||
utf16le[i * 2] = utf16[i] & 0xff | ||
utf16le[i * 2 + 1] = utf16[i] >> 8 | ||
} | ||
|
||
return await md4(utf16le) | ||
} |
Oops, something went wrong.