Skip to content

Commit

Permalink
feat: Initial user will be admin, Make console panel searchable
Browse files Browse the repository at this point in the history
  • Loading branch information
Mrkazik99 committed Aug 31, 2024
1 parent 26ca965 commit ec31ac6
Show file tree
Hide file tree
Showing 20 changed files with 376 additions and 187 deletions.
5 changes: 3 additions & 2 deletions idp/package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 1 addition & 1 deletion idp/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,7 @@
"hashids": "2.3.0",
"jose": "5.3.0",
"js-yaml": "4.1.0",
"meilisearch": "0.40.0",
"meilisearch": "^0.40.0",
"mime-types": "2.1.35",
"minio": "8.0.0",
"morgan": "1.10.0",
Expand Down
18 changes: 14 additions & 4 deletions idp/src/account/service.ts
Original file line number Diff line number Diff line change
Expand Up @@ -16,13 +16,14 @@ import { hashPassword } from '@/infra/password'
import search, { USER_SEARCH_INDEX } from '@/infra/search'
import { User } from '@/user/model'
import userRepo from '@/user/repo'
import { UserDTO, mapEntity } from '@/user/service'
import {UserDTO, mapEntity, getUserCount} from '@/user/service'

export type AccountCreateOptions = {
email: string
password: string
fullName: string
picture?: string
isAdmin?: boolean
}

export type AccountResetPasswordOptions = {
Expand Down Expand Up @@ -53,6 +54,9 @@ export async function createUser(
if (!(await userRepo.isUsernameAvailable(options.email))) {
throw newError({ code: ErrorCode.UsernameUnavailable })
}
if (await getUserCount() === 0) {
options.isAdmin = true
}
try {
const emailConfirmationToken = newHyphenlessUuid()
const user = await userRepo.insert({
Expand All @@ -64,6 +68,7 @@ export async function createUser(
passwordHash: hashPassword(options.password),
emailConfirmationToken,
createTime: newDateTime(),
isAdmin: options.isAdmin,
})
await search.index(USER_SEARCH_INDEX).addDocuments([
{
Expand All @@ -72,9 +77,8 @@ export async function createUser(
email: user.email,
fullName: user.fullName,
isEmailConfirmed: user.isEmailConfirmed,
isAdmin: user.isAdmin,
isActive: user.isActive,
createTime: user.createTime,
updateTime: user.updateTime
},
])
await sendTemplateMail('email-confirmation', options.email, {
Expand Down Expand Up @@ -106,8 +110,14 @@ export async function confirmEmail(options: AccountConfirmEmailOptions) {
})
await search.index(USER_SEARCH_INDEX).updateDocuments([
{
...user,
id: user.id,
username: user.username,
email: user.email,
fullName: user.fullName,
isEmailConfirmed: user.isEmailConfirmed,
createTime: user.createTime,
updateTime: user.updateTime,
picture: user.picture,
},
])
}
Expand Down
23 changes: 16 additions & 7 deletions idp/src/infra/admin-requests.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,11 +8,18 @@
// by the GNU Affero General Public License v3.0 only, included in the file
// licenses/AGPL.txt.
import { Request } from 'express'
import {User} from "@/user/model";

export interface UserIdPostRequest extends Request {
id: string
}

export type SearchRequest = {
page: string
size: string
query: string
}

export interface UserSuspendPostRequest extends UserIdPostRequest {
suspend: boolean
}
Expand All @@ -21,19 +28,21 @@ export interface UserAdminPostRequest extends UserIdPostRequest {
makeAdmin: boolean
}

export type Pagination = {
page: string
size: string
}

export type UserId = {
id: string
}

export interface PaginatedRequest extends Request {
query: Pagination
export interface SearchPaginatedRequest extends Request {
query: SearchRequest
}

export interface UserIdRequest extends Request {
query: UserId
}

export interface UserSearchResponse {
data: User[]
page: number
size: number
totalElements: number
}
9 changes: 6 additions & 3 deletions idp/src/infra/error.ts
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,8 @@ export enum ErrorCode {
UserSuspended = 'user_suspended',
MissingPermission = 'missing_permission',
UserNotFound = 'user_not_found',
OrphanError = 'orphan_error'
OrphanError = 'orphan_error',
SearchError = 'search_error'
}

const statuses: { [key: string]: number } = {
Expand All @@ -44,7 +45,8 @@ const statuses: { [key: string]: number } = {
[ErrorCode.UserSuspended]: 403,
[ErrorCode.MissingPermission]: 403,
[ErrorCode.UserNotFound]: 404,
[ErrorCode.OrphanError]: 400
[ErrorCode.OrphanError]: 400,
[ErrorCode.SearchError]: 500
}

const userMessages: { [key: string]: string } = {
Expand All @@ -54,7 +56,8 @@ const userMessages: { [key: string]: string } = {
[ErrorCode.InvalidUsernameOrPassword]: 'Invalid username or password.',
[ErrorCode.UserSuspended]: 'User suspended.',
[ErrorCode.MissingPermission]: 'You are not an console',
[ErrorCode.OrphanError]: 'You cannot suspend last console'
[ErrorCode.OrphanError]: 'You cannot suspend last console',
[ErrorCode.SearchError]: 'Search engine encountered error or is not available'
}

export type ErrorData = {
Expand Down
20 changes: 19 additions & 1 deletion idp/src/user/repo.ts
Original file line number Diff line number Diff line change
Expand Up @@ -144,6 +144,24 @@ class UserRepoImpl {
return this.mapList(rows)
}

async listAllByIds(idList: number[]): Promise<User[]> {
const { rowCount, rows } = await client.query(
`SELECT *
FROM "user"
WHERE id = ANY ($1)
ORDER BY create_time`,
[idList],
)
if (rowCount < 1) {
throw newError({
code: ErrorCode.ResourceNotFound,
error: `User list is empty`,
userMessage: `Not found users`
})
}
return this.mapList(rows)
}

async getUserCount(): Promise<number> {
const { rowCount, rows } = await client.query(
`SELECT COUNT(id) as count FROM "user"`,
Expand All @@ -154,7 +172,7 @@ class UserRepoImpl {
error: `Fatal database error (no users present in database)`,
})
}
return rows[0].count
return parseInt(rows[0].count)
}

async isUsernameAvailable(username: string): Promise<boolean> {
Expand Down
17 changes: 4 additions & 13 deletions idp/src/user/router.ts
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@ import multer from 'multer'
import os from 'os'
import passport from 'passport'
import {
PaginatedRequest,
SearchPaginatedRequest,
UserAdminPostRequest,
UserIdRequest,
UserSuspendPostRequest
Expand All @@ -36,8 +36,7 @@ import {
UserUpdateEmailConfirmationOptions,
updateEmailRequest,
updateEmailConfirmation,
getUserListPaginated,
getUserCount, suspendUser, makeAdminUser, getUserByAdmin,
suspendUser, makeAdminUser, getUserByAdmin, searchUserListPaginated
} from './service'

const router = Router()
Expand Down Expand Up @@ -222,18 +221,10 @@ router.get(
router.get(
'/all',
passport.authenticate('jwt', { session: false }),
async (req: PaginatedRequest, res: Response, next: NextFunction) => {
async (req: SearchPaginatedRequest, res: Response, next: NextFunction) => {
try {
checkAdmin(req.header('Authorization'))
res.json({
data: await getUserListPaginated(
parseInt(req.query.page),
parseInt(req.query.size),
),
totalElements: await getUserCount(),
page: req.query.page,
size: req.query.size,
})
res.json(await searchUserListPaginated(req.query.query, parseInt(req.query.size), parseInt(req.query.page)))
} catch (err) {
next(err)
}
Expand Down
91 changes: 80 additions & 11 deletions idp/src/user/service.ts
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,7 @@ import {hashPassword, verifyPassword} from '@/infra/password'
import search, {USER_SEARCH_INDEX} from '@/infra/search'
import {User} from '@/user/model'
import userRepo from '@/user/repo'
import {UserAdminPostRequest, UserSuspendPostRequest} from "@/infra/admin-requests";
import {UserAdminPostRequest, UserSearchResponse, UserSuspendPostRequest} from "@/infra/admin-requests";

export type UserDTO = {
id: string
Expand Down Expand Up @@ -65,11 +65,19 @@ export async function getUserByAdmin(id: string): Promise<User> {
return adminMapEntity(await userRepo.findByID(id))
}

export async function getUserListPaginated(
page: number,
size: number,
): Promise<User[]> {
return await userRepo.listAllPaginated(page, size)
export async function searchUserListPaginated(query: string, size: number, page: number
): Promise<UserSearchResponse> {
if (query && query.length >= 3) {
const users = await search.index(USER_SEARCH_INDEX).search(query, {page: page, hitsPerPage: size}).then((value) =>{
return {
data: value.hits,
totalElements: value.totalHits
}
})
return {data: await userRepo.listAllByIds(users.data.map((value) => {return value.id})), totalElements: users.totalElements, size: size, page: page}
} else {
return {data: await userRepo.listAllPaginated(page, size), totalElements: await userRepo.getUserCount(), size: size, page: page}
}
}

export async function getUserCount(): Promise<number> {
Expand All @@ -88,13 +96,23 @@ export async function updateFullName(
user = await userRepo.update({ id: user.id, fullName: options.fullName })
await search.index(USER_SEARCH_INDEX).updateDocuments([
{
...user,
id: user.id,
username: user.username,
email: user.email,
fullName: user.fullName,
isEmailConfirmed: user.isEmailConfirmed,
createTime: user.createTime,
updateTime: user.updateTime,
picture: user.picture,
},
])
return mapEntity(user)
}

export const raiseSearchError = () => {
throw newError({code: ErrorCode.SearchError})
}

export async function updateEmailRequest(
id: string,
options: UserUpdateEmailRequestOptions,
Expand Down Expand Up @@ -154,11 +172,14 @@ export async function updateEmailConfirmation(
})
await search.index(USER_SEARCH_INDEX).updateDocuments([
{
...user,
id: user.id,
username: user.username,
email: user.email,
username: user.email,
emailUpdateToken: null,
emailUpdateValue: null,
fullName: user.fullName,
isEmailConfirmed: user.isEmailConfirmed,
createTime: user.createTime,
updateTime: user.updateTime,
picture: user.picture,
},
])
return mapEntity(user)
Expand Down Expand Up @@ -191,12 +212,36 @@ export async function updatePicture(
id: userId,
picture: `data:${contentType};base64,${picture}`,
})
await search.index(USER_SEARCH_INDEX).updateDocuments([
{
id: user.id,
username: user.username,
email: user.email,
fullName: user.fullName,
isEmailConfirmed: user.isEmailConfirmed,
createTime: user.createTime,
updateTime: user.updateTime,
picture: user.picture,
},
])
return mapEntity(user)
}

export async function deletePicture(id: string): Promise<UserDTO> {
let user = await userRepo.findByID(id)
user = await userRepo.update({ id: user.id, picture: null })
await search.index(USER_SEARCH_INDEX).updateDocuments([
{
id: user.id,
username: user.username,
email: user.email,
fullName: user.fullName,
isEmailConfirmed: user.isEmailConfirmed,
createTime: user.createTime,
updateTime: user.updateTime,
picture: user.picture,
},
])
return mapEntity(user)
}

Expand All @@ -217,6 +262,18 @@ export async function suspendUser(options: UserSuspendPostRequest) {
}
if (user) {
await userRepo.suspend(user.id, options.suspend)
await search.index(USER_SEARCH_INDEX).updateDocuments([
{
id: user.id,
username: user.username,
email: user.email,
fullName: user.fullName,
isEmailConfirmed: user.isEmailConfirmed,
createTime: user.createTime,
updateTime: user.updateTime,
picture: user.picture,
},
])
} else {
throw newError({ code: ErrorCode.UserNotFound })
}
Expand All @@ -229,6 +286,18 @@ export async function makeAdminUser(options: UserAdminPostRequest) {
}
if (user) {
await userRepo.makeAdmin(user.id, options.makeAdmin)
await search.index(USER_SEARCH_INDEX).updateDocuments([
{
id: user.id,
username: user.username,
email: user.email,
fullName: user.fullName,
isEmailConfirmed: user.isEmailConfirmed,
createTime: user.createTime,
updateTime: user.updateTime,
picture: user.picture,
},
])
} else {
throw newError({ code: ErrorCode.UserNotFound })
}
Expand Down
Loading

0 comments on commit ec31ac6

Please sign in to comment.