Skip to content

Commit

Permalink
feat: feedback detail (#66)
Browse files Browse the repository at this point in the history
* feat: add feedback detail component

* fix: jwt expired time, project box, feedback table deletion reload

* feat: feedback detail component

* fix: auth service spec
  • Loading branch information
chiol authored Oct 24, 2023
1 parent 9226ef0 commit 0aad41e
Show file tree
Hide file tree
Showing 14 changed files with 247 additions and 59 deletions.
5 changes: 4 additions & 1 deletion apps/api/.env.example
Original file line number Diff line number Diff line change
Expand Up @@ -22,4 +22,7 @@ APP_ADDRESS= # default: 0.0.0.0
AUTO_MIGRATION= # default: false

MASTER_API_KEY= # default: none
BASE_URL= # default: http://localhost:3000
BASE_URL= # default: http://localhost:3000

ACCESS_TOKEN_EXPIRED_TIME= # default: 10m
REFESH_TOKEN_EXPIRED_TIME= # default: 1h
44 changes: 23 additions & 21 deletions apps/api/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -70,27 +70,29 @@ npm run migration:run

## Environment Variables

| Environment | Description | Default Value |
| -------------------- | -------------------------------------------------------------------------------- | ------------------------------------------------------------------ |
| JWT_SECRET | JWT secret | # required |
| MYSQL_PRIMARY_URL | mysql url | mysql://userfeedback:userfeedback@localhost:13306/userfeedback |
| MYSQL_SECONDARY_URLS | mysql sub urls (must be json array format) | ["mysql://userfeedback:userfeedback@localhost:13306/userfeedback"] |
| SMTP_USE | flag for using smtp server (for email verification on creating user) | false |
| SMTP_HOST | smtp server host | localhost |
| SMTP_PORT | smtp server port | 25 |
| SMTP_USERNAME | smtp auth username | |
| SMTP_PASSWORD | smtp auth password | |
| SMTP_SENDER | mail sender email | [email protected] |
| SMTP_BASE_URL | default UserFeedback URL for mail to be redirected | http://localhost:3000 |
| APP_PORT | the post that the server is running on | 4000 |
| APP_ADDRESS | the address that the server is running on | 0.0.0.0 |
| OS_USE | flag for using opensearch (for better performance on searching feedbacks) | false |
| OS_NODE | opensearch node url | http://localhost:9200 |
| OS_USERNAME | opensearch username if exists | |
| OS_PASSWORD | opensearch password if exists | |
| AUTO_MIGRATION | set 'true' if you want to make the database migration automatically | |
| MASTER_API_KEY | set a key if you want to make a master key for creating feedback | |
| NODE_OPTIONS | set some options if you want to add for node execution (e.g. max_old_space_size) | |
| Environment | Description | Default Value |
| ------------------------- | -------------------------------------------------------------------------------- | ------------------------------------------------------------------ |
| JWT_SECRET | JWT secret | # required |
| MYSQL_PRIMARY_URL | mysql url | mysql://userfeedback:userfeedback@localhost:13306/userfeedback |
| MYSQL_SECONDARY_URLS | mysql sub urls (must be json array format) | ["mysql://userfeedback:userfeedback@localhost:13306/userfeedback"] |
| SMTP_USE | flag for using smtp server (for email verification on creating user) | false |
| SMTP_HOST | smtp server host | localhost |
| SMTP_PORT | smtp server port | 25 |
| SMTP_USERNAME | smtp auth username | |
| SMTP_PASSWORD | smtp auth password | |
| SMTP_SENDER | mail sender email | [email protected] |
| SMTP_BASE_URL | default UserFeedback URL for mail to be redirected | http://localhost:3000 |
| APP_PORT | the post that the server is running on | 4000 |
| APP_ADDRESS | the address that the server is running on | 0.0.0.0 |
| OS_USE | flag for using opensearch (for better performance on searching feedbacks) | false |
| OS_NODE | opensearch node url | http://localhost:9200 |
| OS_USERNAME | opensearch username if exists | |
| OS_PASSWORD | opensearch password if exists | |
| AUTO_MIGRATION | set 'true' if you want to make the database migration automatically | |
| MASTER_API_KEY | set a key if you want to make a master key for creating feedback | |
| NODE_OPTIONS | set some options if you want to add for node execution (e.g. max_old_space_size) | |
| ACCESS_TOKEN_EXPIRED_TIME | set expired time of access token | 10m |
| REFESH_TOKEN_EXPIRED_TIME | set expired time of refresh token | 1h |

## Swagger

Expand Down
1 change: 1 addition & 0 deletions apps/api/src/common/repositories/opensearch.repository.ts
Original file line number Diff line number Diff line change
Expand Up @@ -186,6 +186,7 @@ export class OpensearchRepository {
await this.opensearchClient.deleteByQuery({
index,
body: { query: { terms: { _id: ids } } },
refresh: true,
});
}

Expand Down
4 changes: 4 additions & 0 deletions apps/api/src/configs/jwt.config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -18,8 +18,12 @@ import * as yup from 'yup';

export const jwtConfigSchema = yup.object({
JWT_SECRET: yup.string().required(),
ACCESS_TOKEN_EXPIRED_TIME: yup.string().default('10m'),
REFESH_TOKEN_EXPIRED_TIME: yup.string().default('1h'),
});

export const jwtConfig = registerAs('jwt', () => ({
secret: process.env.JWT_SECRET,
accessTokenExpiredTime: process.env.ACCESS_TOKEN_EXPIRED_TIME,
refreshTokenExpiredTime: process.env.REFESH_TOKEN_EXPIRED_TIME,
}));
2 changes: 2 additions & 0 deletions apps/api/src/domains/auth/auth.service.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@ import type { Repository } from 'typeorm';

import { CodeEntity } from '@/shared/code/code.entity';
import { NotVerifiedEmailException } from '@/shared/mailing/exceptions';
import { TestConfig } from '@/test-utils/util-functions';
import {
AuthServiceProviders,
MockEmailVerificationMailingService,
Expand Down Expand Up @@ -52,6 +53,7 @@ describe('auth service ', () => {
let apiKeyRepo: Repository<ApiKeyEntity>;
beforeEach(async () => {
const module = await Test.createTestingModule({
imports: [TestConfig],
providers: AuthServiceProviders,
}).compile();
authService = module.get(AuthService);
Expand Down
15 changes: 8 additions & 7 deletions apps/api/src/domains/auth/auth.service.ts
Original file line number Diff line number Diff line change
Expand Up @@ -19,8 +19,8 @@ import {
BadRequestException,
Injectable,
InternalServerErrorException,
Logger,
} from '@nestjs/common';
import { ConfigService } from '@nestjs/config';
import { JwtService } from '@nestjs/jwt';
import { AxiosError } from 'axios';
import * as bcrypt from 'bcrypt';
Expand All @@ -30,6 +30,7 @@ import { Transactional } from 'typeorm-transactional';

import { EmailVerificationMailingService } from '@/shared/mailing/email-verification-mailing.service';
import { NotVerifiedEmailException } from '@/shared/mailing/exceptions';
import type { ConfigServiceType } from '@/types/config-service.type';
import { CodeTypeEnum } from '../../shared/code/code-type.enum';
import { CodeService } from '../../shared/code/code.service';
import { ApiKeyService } from '../project/api-key/api-key.service';
Expand Down Expand Up @@ -59,7 +60,6 @@ import { PasswordNotMatchException, UserBlockedException } from './exceptions';

@Injectable()
export class AuthService {
private logger = new Logger(AuthService.name);
private REDIRECT_URI = `${process.env.BASE_URL}/auth/oauth-callback`;

constructor(
Expand All @@ -72,6 +72,7 @@ export class AuthService {
private readonly tenantService: TenantService,
private readonly roleService: RoleService,
private readonly memberService: MemberService,
private readonly configService: ConfigService<ConfigServiceType>,
private readonly httpService: HttpService,
) {}

Expand Down Expand Up @@ -172,18 +173,18 @@ export class AuthService {
const { email, id, department, name, type } = user;
const { state } = await this.userService.findById(id);

if (state === UserStateEnum.Blocked) {
throw new UserBlockedException();
}
if (state === UserStateEnum.Blocked) throw new UserBlockedException();
const { accessTokenExpiredTime, refreshTokenExpiredTime } =
this.configService.get('jwt', { infer: true });

return {
accessToken: this.jwtService.sign(
{ sub: id, email, department, name, type },
{ expiresIn: '10m' },
{ expiresIn: accessTokenExpiredTime },
),
refreshToken: this.jwtService.sign(
{ sub: id, email },
{ expiresIn: '1h' },
{ expiresIn: refreshTokenExpiredTime },
),
};
}
Expand Down
3 changes: 2 additions & 1 deletion apps/api/src/test-utils/util-functions.ts
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@ import { ConfigModule } from '@nestjs/config';
import type { DataSource, Repository } from 'typeorm';
import { initializeTransactionalContext } from 'typeorm-transactional';

import { jwtConfig } from '@/configs/jwt.config';
import { smtpConfig, smtpConfigSchema } from '@/configs/smtp.config';
import type { AuthService } from '@/domains/auth/auth.service';
import { UserDto } from '@/domains/user/dtos';
Expand All @@ -33,7 +34,7 @@ export const getMockProvider = (
): Provider => ({ provide: injectToken, useFactory: () => factory });

export const TestConfig = ConfigModule.forRoot({
load: [smtpConfig],
load: [smtpConfig, jwtConfig],
envFilePath: '.env.test',
validate: (config) => ({
...smtpConfigSchema.validateSync(config),
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -34,7 +34,7 @@ const ChannelSelectBox: React.FC<IProps> = ({ onChangeChannel }) => {
key={channel.id}
onClick={() => onChangeChannel(channel.id)}
className={[
'flex h-10 min-w-[136px] cursor-pointer items-center gap-2 rounded border px-3 py-2.5',
'flex h-10 min-w-[136px] cursor-pointer items-center justify-between gap-2 rounded border px-3 py-2.5',
channel.id === channelId ? 'border-fill-primary' : 'opacity-50',
].join(' ')}
>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -112,10 +112,13 @@ const DownloadButton: React.FC<IDownloadButtonProps> = ({
disabled={!perms.includes('feedback_download_read')}
onClick={() => setOpen((prev) => !prev)}
>
<Icon name="Download" size={16} />
{isHead
? t('main.feedback.button.select-download', { count })
: t('main.feedback.button.all-download')}
<div className="flex gap-1">
<Icon name="Download" size={16} />
{isHead
? t('main.feedback.button.select-download', { count })
: t('main.feedback.button.all-download')}
</div>
<Icon name="ChevronDown" size={12} />
</PopoverTrigger>
<PopoverContent>
<p className="font-12-bold px-3 py-3">
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,146 @@
/**
* Copyright 2023 LINE Corporation
*
* LINE Corporation licenses this file to you under the Apache License,
* version 2.0 (the "License"); you may not use this file except in compliance
* with the License. You may obtain a copy of the License at:
*
* https://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
* WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
* License for the specific language governing permissions and limitations
* under the License.
*/
import {
autoUpdate,
FloatingFocusManager,
FloatingOverlay,
FloatingPortal,
useClick,
useDismiss,
useFloating,
useInteractions,
useRole,
} from '@floating-ui/react';
import dayjs from 'dayjs';

import { Badge, Icon } from '@ufb/ui';

import { DATE_TIME_FORMAT } from '@/constants/dayjs-format';
import { getStatusColor } from '@/constants/issues';
import { useFeedbackSearch, useOAIQuery } from '@/hooks';
import type { FieldType } from '@/types/field.type';
import type { IssueType } from '@/types/issue.type';

interface IProps {
id: number;
projectId: number;
channelId: number;
open: boolean;
onOpenChange: (open: boolean) => void;
}

const FeedbackDetail: React.FC<IProps> = (props) => {
const { channelId, id, projectId, onOpenChange, open } = props;
const { data } = useFeedbackSearch(projectId, channelId, {
query: { ids: [id] },
});
const feedbackData = data?.items?.[0] ?? {};

const { data: channelData } = useOAIQuery({
path: '/api/projects/{projectId}/channels/{channelId}',
variables: { channelId, projectId },
});

const { refs, context } = useFloating({
open,
onOpenChange,
whileElementsMounted: autoUpdate,
});
const click = useClick(context);
const dismiss = useDismiss(context);
const role = useRole(context);

const { getFloatingProps } = useInteractions([click, dismiss, role]);

return (
<FloatingPortal>
<FloatingFocusManager context={context} modal>
<FloatingOverlay
lockScroll={true}
className="bg-dim"
style={{ display: 'grid', placeItems: 'center', zIndex: 20 }}
>
<div
className="bg-primary fixed right-0 top-0 h-screen w-[760px] overflow-auto"
ref={refs.setFloating}
{...getFloatingProps()}
onClick={(e) => e.stopPropagation()}
>
<div className="overflow-y-auto p-10">
<div className="flex items-center">
<h1 className="font-20-bold flex-1">피드백 상세</h1>
<button
className="icon-btn icon-btn-tertiary icon-btn-md"
onClick={() => context.onOpenChange(false)}
>
<Icon name="CloseCircleFill" size={20} />
</button>
</div>
<table className="border-separate border-spacing-y-5">
<colgroup>
<col style={{ minWidth: 80 }} />
</colgroup>
<tbody>
{channelData?.fields.sort(fieldSortType).map((field) => (
<tr key={field.name}>
<th className="font-14-regular text-secondary mr-2 text-left align-text-top">
{field.name}
</th>
<td className="font-14-regular text-primary">
{field.key === 'issues' ? (
<div className="flex gap-2">
{(
feedbackData[field.key] ?? ([] as IssueType[])
).map((v) => (
<Badge
key={v.id}
color={getStatusColor(v.status)}
type="secondary"
>
{v.name}
</Badge>
))}
</div>
) : field.format === 'multiSelect' ? (
(feedbackData[field.key] ?? ([] as string[])).join(
', ',
)
) : field.format === 'date' ? (
dayjs(feedbackData[field.key]).format(
DATE_TIME_FORMAT,
)
) : (
feedbackData[field.key]
)}
</td>
</tr>
))}
</tbody>
</table>
</div>
</div>
</FloatingOverlay>
</FloatingFocusManager>
</FloatingPortal>
);
};
const fieldSortType = (a: FieldType, b: FieldType) => {
const aNum = a.type === 'DEFAULT' ? 1 : a.type === 'API' ? 2 : 3;
const bNum = b.type === 'DEFAULT' ? 1 : b.type === 'API' ? 2 : 3;
return aNum - bNum;
};

export default FeedbackDetail;
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
/**
* Copyright 2023 LINE Corporation
*
* LINE Corporation licenses this file to you under the Apache License,
* version 2.0 (the "License"); you may not use this file except in compliance
* with the License. You may obtain a copy of the License at:
*
* https://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
* WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
* License for the specific language governing permissions and limitations
* under the License.
*/
export { default } from './FeedbackDetail';
Loading

0 comments on commit 0aad41e

Please sign in to comment.