Skip to content

Commit

Permalink
Merge pull request #67 from line/dev
Browse files Browse the repository at this point in the history
beta release: 3.2343.14-beta
  • Loading branch information
h4l-yup authored Oct 24, 2023
2 parents fc2f123 + 0aad41e commit ae9d62b
Show file tree
Hide file tree
Showing 17 changed files with 317 additions and 104 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/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,7 @@
"dependencies": {
"@fastify/static": "^6.11.2",
"@nestjs-modules/mailer": "^1.9.1",
"@nestjs/axios": "^3.0.0",
"@nestjs/common": "^10.2.7",
"@nestjs/config": "^3.1.1",
"@nestjs/core": "^10.2.7",
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,
}));
5 changes: 5 additions & 0 deletions apps/api/src/domains/auth/auth.module.ts
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@
* License for the specific language governing permissions and limitations
* under the License.
*/
import { HttpModule } from '@nestjs/axios';
import { Module } from '@nestjs/common';
import { ConfigModule, ConfigService } from '@nestjs/config';
import { JwtModule } from '@nestjs/jwt';
Expand Down Expand Up @@ -41,6 +42,10 @@ import { LocalStrategy } from './strategies/local.strategy';
TenantModule,
RoleModule,
MemberModule,
HttpModule.register({
timeout: 5000,
maxRedirects: 5,
}),
JwtModule.registerAsync({
global: true,
imports: [ConfigModule],
Expand Down
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
116 changes: 64 additions & 52 deletions apps/api/src/domains/auth/auth.service.ts
Original file line number Diff line number Diff line change
Expand Up @@ -14,20 +14,23 @@
* under the License.
*/
import crypto from 'crypto';
import { HttpService } from '@nestjs/axios';
import {
BadRequestException,
Injectable,
InternalServerErrorException,
Logger,
} from '@nestjs/common';
import { ConfigService } from '@nestjs/config';
import { JwtService } from '@nestjs/jwt';
import axios, { AxiosError } from 'axios';
import { AxiosError } from 'axios';
import * as bcrypt from 'bcrypt';
import dayjs from 'dayjs';
import { catchError, lastValueFrom, map } from 'rxjs';
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 @@ -57,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 @@ -70,6 +72,8 @@ export class AuthService {
private readonly tenantService: TenantService,
private readonly roleService: RoleService,
private readonly memberService: MemberService,
private readonly configService: ConfigService<ConfigServiceType>,
private readonly httpService: HttpService,
) {}

async sendEmailCode({ email }: SendEmailCodeDto) {
Expand Down Expand Up @@ -169,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 Expand Up @@ -222,7 +226,7 @@ export class AuthService {
return `${oauthConfig.authCodeRequestURL}?${params}`;
}

private async getAccessToken(code: string) {
private async getAccessToken(code: string): Promise<string> {
const { oauthConfig, useOAuth } = await this.tenantService.findOne();

if (!useOAuth) {
Expand All @@ -233,35 +237,39 @@ export class AuthService {
}

const { accessTokenRequestURL, clientId, clientSecret } = oauthConfig;
try {
const { data } = await axios.post(
accessTokenRequestURL,
{
grant_type: 'authorization_code',
code,
redirect_uri: this.REDIRECT_URI,
},
{
headers: {
Authorization: `Basic ${Buffer.from(
clientId + ':' + clientSecret,
).toString('base64')}`,
'Content-Type': 'application/x-www-form-urlencoded',
return await lastValueFrom(
this.httpService
.post(
accessTokenRequestURL,
{
grant_type: 'authorization_code',
code,
redirect_uri: this.REDIRECT_URI,
},
},
);
return data.access_token;
} catch (error) {
if (error instanceof AxiosError) {
throw new InternalServerErrorException({
axiosError: {
...error.response.data,
status: error.response.status,
{
headers: {
Authorization: `Basic ${Buffer.from(
clientId + ':' + clientSecret,
).toString('base64')}`,
'Content-Type': 'application/x-www-form-urlencoded',
},
},
});
}
throw error;
}
)
.pipe(map((res) => res.data?.access_token))
.pipe(
catchError((error) => {
if (error instanceof AxiosError) {
throw new InternalServerErrorException({
axiosError: {
...error.response.data,
status: error.response.status,
},
});
}
throw error;
}),
),
);
}

private async getEmailByAccessToken(accessToken: string): Promise<string> {
Expand All @@ -270,22 +278,26 @@ export class AuthService {
if (!oauthConfig) {
throw new BadRequestException('OAuth Config is required.');
}
try {
const { data } = await axios.get(oauthConfig.userProfileRequestURL, {
headers: { Authorization: `Bearer ${accessToken}` },
});
return data[oauthConfig.emailKey];
} catch (error) {
if (error instanceof AxiosError) {
throw new InternalServerErrorException({
axiosError: {
...error.response.data,
status: error.response.status,
},
});
}
throw error;
}
return await lastValueFrom(
this.httpService
.get(oauthConfig.userProfileRequestURL, {
headers: { Authorization: `Bearer ${accessToken}` },
})
.pipe(map((res) => res.data?.[oauthConfig.emailKey]))
.pipe(
catchError((error) => {
if (error instanceof AxiosError) {
throw new InternalServerErrorException({
axiosError: {
...error.response.data,
status: error.response.status,
},
});
}
throw error;
}),
),
);
}

async signInByOAuth(code: string) {
Expand Down
8 changes: 8 additions & 0 deletions apps/api/src/test-utils/providers/auth.service.providers.ts
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@
* License for the specific language governing permissions and limitations
* under the License.
*/
import { HttpService } from '@nestjs/axios';
import { JwtService } from '@nestjs/jwt';
import { ClsService } from 'nestjs-cls';

Expand Down Expand Up @@ -49,4 +50,11 @@ export const AuthServiceProviders = [
...RoleServiceProviders,
...MemberServiceProviders,
ClsService,
{
provide: HttpService,
useValue: {
get: jest.fn(),
post: jest.fn(),
},
},
];
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
Loading

0 comments on commit ae9d62b

Please sign in to comment.