all: publish code

This commit is contained in:
Nugraha 2022-12-02 20:40:23 +07:00
commit f3db64fa38
Signed by: ii64
GPG key ID: E41C08AD390E7C49
100 changed files with 73612 additions and 0 deletions

1
.env Normal file
View file

@ -0,0 +1 @@
SECRET=hello_world

17
.gitignore vendored Normal file
View file

@ -0,0 +1,17 @@
# Personal
docs/
app.db
docker/server
server
# Editor directories and files
.vscode/*
!.vscode/tasks.json
!.vscode/launch.json
.idea
.DS_Store
*.suo
*.ntvs*
*.njsproj
*.sln
*.sw?

22
.vscode/launch.json vendored Normal file
View file

@ -0,0 +1,22 @@
{
"configurations": [
{
"name": "Launch server on external",
"type": "go",
"request": "launch",
"mode": "auto",
"program": "${workspaceFolder}/cmd/server",
"host": "127.0.0.1",
"port": 10500,
"showGlobalVariables": true,
},
{
"name": "Launch server",
"type": "go",
"request": "launch",
"mode": "auto",
"program": "${workspaceFolder}/cmd/server",
"showGlobalVariables": true,
}
]
}

11
.vscode/tasks.json vendored Normal file
View file

@ -0,0 +1,11 @@
{
"version": "2.0.0",
"tasks": [
{
"label": "Run dlv",
"type": "shell",
"command": "dlv dap --check-go-version --listen 127.0.0.1:10500",
"group": "build",
},
],
}

55
Makefile Normal file
View file

@ -0,0 +1,55 @@
include Makefile.common
include Makefile.sthrift
include Makefile.autogen
# use zig cc/c++ to statically link deps
TARGET_TRIPLE := x86_64-linux
CFLAGS ?=
CFLAGS += -target $(TARGET_TRIPLE)
CXXFLAGS ?=
CXXFLAGS += -target $(TARGET_TRIPLE)
GOFLAGS ?=
GOFLAGS += -x -o docker/server
all:
gen: gen-backend-thrift gen-frontend-thrift
clean: clean-public
clean-public:
rm -r public/
mkdir public
echo "$$_G_PUBLIC_EXPORTER" > public/public.go
build: frontend-build backend-build
build-dev: frontend-debug backend-build
build.docker: build
"docker" build -t f-ass-wpw:dev docker/
backend-dev:
go run github.com/ii64/go-dlv-manager@latest
backend-build: clean-public
CC="zig cc $(CFLAGS)" CXX="zig c++ $(CXXFLAGS)" go build $(GOFLAGS) wpw-common/cmd/server
frontend-dev:
$(MAKE) -C frontend dev
frontend-debug: clean-public
$(MAKE) -C frontend build ENV=dev
frontend-build: clean-public
$(MAKE) -C frontend build ENV=production
frontend-preview:
$(MAKE) -C frontend preview
gen-backend-thrift:
$(MAKE) gen-idl \
THRIFT_DIR_SRC=$(THRIFT_IDL_DIR) \
THRIFT_DIR_OUT=$(THRIFT_GEN_DIR)
gen-frontend-thrift:
$(MAKE) gen-idl \
THRIFT=thrift \
THRIFTGO_GEN=js:"node,ts,es6" \
THRIFT_DIR_SRC=$(THRIFT_IDL_DIR) \
THRIFT_DIR_OUT=$(FRONTEND_THRIFT_GEN_DIR)

9
Makefile.autogen Normal file
View file

@ -0,0 +1,9 @@
define _G_PUBLIC_EXPORTER
package public
import "embed"
//go:embed *
var FS embed.FS
endef
export _G_PUBLIC_EXPORTER

7
Makefile.common Normal file
View file

@ -0,0 +1,7 @@
PKG := wpw-common
THRIFT_IDL_DIR := idl
THRIFT_LIB := github.com/apache/thrift/lib/go/thrift
THRIFT_GEN_PACKAGE_PREFIX := $(PKG)/pkg/gen
THRIFT_GEN_DIR := pkg/gen
FRONTEND_THRIFT_GEN_DIR := frontend/gen

34
Makefile.sthrift Normal file
View file

@ -0,0 +1,34 @@
include Makefile.common
THRIFT_GEN_REMOTE_PATTERN := *remote
THRIFTGO_GEN_FLAG := thrift_import_path=$(THRIFT_LIB),package_prefix=$(THRIFT_GEN_PACKAGE_PREFIX)
THRIFTGO_GEN_FLAG := $(THRIFTGO_GEN_FLAG),reorder_fields=true
THRIFTGO_GEN_FLAG := $(THRIFTGO_GEN_FLAG),frugal_tag=true
THRIFTGO_GEN_FLAG := $(THRIFTGO_GEN_FLAG),keep_unknown_fields=true
THRIFTGO_GEN_FLAG := $(THRIFTGO_GEN_FLAG),reserve_comments=true
THRIFTGO_GEN_FLAG := $(THRIFTGO_GEN_FLAG),nil_safe=false
THRIFTGO_GEN_FLAG := $(THRIFTGO_GEN_FLAG),compatible_names=true
THRIFTGO_GEN_FLAG := $(THRIFTGO_GEN_FLAG),gen_type_meta=true
# THRIFTGO_GEN_FLAG := $(THRIFTGO_GEN_FLAG),value_type_in_container=true
THRIFTGO_GEN_FLAG := $(THRIFTGO_GEN_FLAG),validate_set=true
THRIFTGO_GEN_FLAG := $(THRIFTGO_GEN_FLAG),use_type_alias=true
THRIFTGO_GEN_FLAG := $(THRIFTGO_GEN_FLAG),gen_db_tag=true
THRIFTGO_GEN_FLAG := $(THRIFTGO_GEN_FLAG),gen_setter=true
THRIFTGO_GEN := go:"$(THRIFTGO_GEN_FLAG)"
THRIFT := thriftgo
# gen-idl need `THRIFT_DIR_SRC` as input, and `THRIFT_DIR_OUT` as output.
.PHONY: gen-idl
gen-idl:
go run github.com/ii64/thrift-idl-builder \
-errors \
-wrk 10 \
-source-dir $(THRIFT_DIR_SRC) \
-o $(THRIFT_DIR_OUT) \
-bin $(THRIFT) \
-gen $(THRIFTGO_GEN) && \
bash -c 'find $(THRIFTGO_DIR_OUT) -name "$(THRIFT_GEN_REMOTE_PATTERN)" -prune -exec bash -c "echo {} && rm -r {}" \;' && \
echo OK

20
README.md Normal file
View file

@ -0,0 +1,20 @@
## Project-Title
This is a Go+Vite(Vue) template that just quickly written to fulfil the need of personal stuff.
Consider to read existing implementations before using.
Tech Stack used: Go, fasthttp, Apache Thrift/ThriftGo, GORM, Vite + Vue3 + TS
Development:
Ctrl+Shift+B - run instance
F5 - attach debug
make frontend-dev
Run:
make build.docker
docker run --rm -e SECRET=abc f-ass-wpw:dev
MIT. ntsc

7
docker/Dockerfile Normal file
View file

@ -0,0 +1,7 @@
FROM alpine:latest
WORKDIR /app
# yes, cheat. Ideally I would use multi stage build here
# but in this case I'll just throw this away.
COPY server /app/server
ENTRYPOINT [ "/app/server" ]

7
frontend/.babelrc Normal file
View file

@ -0,0 +1,7 @@
{
"plugins": [
"minify-dead-code-elimination",
"@babel/plugin-transform-classes",
"@babel/plugin-syntax-class-properties",
]
}

1
frontend/.env Normal file
View file

@ -0,0 +1 @@
VITE_HUB_URL=http://127.0.0.1:8000/api/core/v1

2
frontend/.env.production Normal file
View file

@ -0,0 +1,2 @@
# VITE_HUB_URL=https://int-cip.i64.tech/api/core/v1
VITE_HUB_URL=http://127.0.0.1:8000/api/core/v1

18
frontend/.eslintrc.cjs Normal file
View file

@ -0,0 +1,18 @@
/* eslint-env node */
require('@rushstack/eslint-patch/modern-module-resolution');
module.exports = {
root: true,
rules: {
'@typescript-eslint/ban-ts-comment': ['error', { 'ts-ignore': 'allow-with-description' }],
// '@typescript-eslint/ban-ts-ignore': 'off',
// '@typescript-eslint/ban-ts-comment': 'off',
},
extends: [
// 'plugin:vue/vue3-essential',
'plugin:prettier/recommended',
'eslint:recommended',
'@vue/eslint-config-typescript/recommended',
'@vue/eslint-config-prettier',
],
};

26
frontend/.gitignore vendored Normal file
View file

@ -0,0 +1,26 @@
# Logs
logs
*.log
npm-debug.log*
yarn-debug.log*
yarn-error.log*
pnpm-debug.log*
lerna-debug.log*
node_modules
node_modules_linux
node_modules_windows
dist
dist-ssr
*.local
# Editor directories and files
.vscode/*
!.vscode/extensions.json
.idea
.DS_Store
*.suo
*.ntvs*
*.njsproj
*.sln
*.sw?

8
frontend/.prettierrc.cjs Normal file
View file

@ -0,0 +1,8 @@
module.exports = {
semi: true,
trailingComma: "all",
singleQuote: true,
printWidth: 100,
tabWidth: 2,
endOfLine:"auto"
};

3
frontend/.vscode/extensions.json vendored Normal file
View file

@ -0,0 +1,3 @@
{
"recommendations": ["Vue.volar"]
}

17
frontend/Makefile Normal file
View file

@ -0,0 +1,17 @@
ifdef ENV
ENV_FLAG := --mode $(ENV)
endif
all: dev
typecheck:
pnpm run typecheck
lint:
pnpm run lint
dev:
pnpm run dev
build:
pnpm run build $(ENV_FLAG)
preview:
pnpm run preview

16
frontend/README.md Normal file
View file

@ -0,0 +1,16 @@
# Vue 3 + TypeScript + Vite
This template should help get you started developing with Vue 3 and TypeScript in Vite. The template uses Vue 3 `<script setup>` SFCs, check out the [script setup docs](https://v3.vuejs.org/api/sfc-script-setup.html#sfc-script-setup) to learn more.
## Recommended IDE Setup
- [VS Code](https://code.visualstudio.com/) + [Volar](https://marketplace.visualstudio.com/items?itemName=Vue.volar)
## Type Support For `.vue` Imports in TS
Since TypeScript cannot handle type information for `.vue` imports, they are shimmed to be a generic Vue component type by default. In most cases this is fine if you don't really care about component prop types outside of templates. However, if you wish to get actual prop types in `.vue` imports (for example to get props validation when using manual `h(...)` calls), you can enable Volar's Take Over mode by following these steps:
1. Run `Extensions: Show Built-in Extensions` from VS Code's command palette, look for `TypeScript and JavaScript Language Features`, then right click and select `Disable (Workspace)`. By default, Take Over mode will enable itself if the default TypeScript extension is disabled.
2. Reload the VS Code window by running `Developer: Reload Window` from the command palette.
You can learn more about Take Over mode [here](https://github.com/johnsoncodehk/volar/discussions/471).

9
frontend/env.d.ts vendored Normal file
View file

@ -0,0 +1,9 @@
/// <reference types="vite/client" />
interface ImportMetaEnv {
readonly VITE_HUB_URL: string;
}
interface ImportMeta {
readonly env: ImportMetaEnv;
}

75
frontend/gen/CoreService.d.ts vendored Normal file
View file

@ -0,0 +1,75 @@
//
// Autogenerated by Thrift Compiler (0.16.0)
//
// DO NOT EDIT UNLESS YOU ARE SURE THAT YOU KNOW WHAT YOU ARE DOING
//
import thrift = require('thrift');
import Thrift = thrift.Thrift;
import Q = thrift.Q;
import Int64 = require('node-int64');
import structs_ttypes = require('./structs_types');
import exceptions_ttypes = require('./exceptions_types');
import ttypes = require('./service_types');
declare class Client {
private output: thrift.TTransport;
private pClass: thrift.TProtocol;
private _seqid: number;
constructor(output: thrift.TTransport, pClass: { new(trans: thrift.TTransport): thrift.TProtocol });
login(request: structs_ttypes.LoginRequest): Promise<structs_ttypes.LoginResponse>;
login(request: structs_ttypes.LoginRequest, callback?: (error: exceptions_ttypes.CoreServicesException, response: structs_ttypes.LoginResponse)=>void): void;
getProfile(request: structs_ttypes.GetProfileRequest): Promise<structs_ttypes.GetProfileResponse>;
getProfile(request: structs_ttypes.GetProfileRequest, callback?: (error: exceptions_ttypes.CoreServicesException, response: structs_ttypes.GetProfileResponse)=>void): void;
logout(request: structs_ttypes.LogoutRequest): Promise<structs_ttypes.LogoutResponse>;
logout(request: structs_ttypes.LogoutRequest, callback?: (error: exceptions_ttypes.CoreServicesException, response: structs_ttypes.LogoutResponse)=>void): void;
getUserList(request: structs_ttypes.GetUserListRequest): Promise<structs_ttypes.GetUserListResponse>;
getUserList(request: structs_ttypes.GetUserListRequest, callback?: (error: exceptions_ttypes.CoreServicesException, response: structs_ttypes.GetUserListResponse)=>void): void;
createUser(request: structs_ttypes.CreateUserRequest): Promise<structs_ttypes.CreateUserResponse>;
createUser(request: structs_ttypes.CreateUserRequest, callback?: (error: exceptions_ttypes.CoreServicesException, response: structs_ttypes.CreateUserResponse)=>void): void;
deleteUsers(request: structs_ttypes.DeleteUsersRequest): Promise<structs_ttypes.DeleteUsersResponse>;
deleteUsers(request: structs_ttypes.DeleteUsersRequest, callback?: (error: exceptions_ttypes.CoreServicesException, response: structs_ttypes.DeleteUsersResponse)=>void): void;
getWajibPajakList(request: structs_ttypes.GetWajibPajakListRequest): Promise<structs_ttypes.GetWajibPajakListResponse>;
getWajibPajakList(request: structs_ttypes.GetWajibPajakListRequest, callback?: (error: exceptions_ttypes.CoreServicesException, response: structs_ttypes.GetWajibPajakListResponse)=>void): void;
createWajibPajak(request: structs_ttypes.CreateWajibPajakRequest): Promise<structs_ttypes.CreateWajibPajakResponse>;
createWajibPajak(request: structs_ttypes.CreateWajibPajakRequest, callback?: (error: exceptions_ttypes.CoreServicesException, response: structs_ttypes.CreateWajibPajakResponse)=>void): void;
deleteWajibPajakList(request: structs_ttypes.DeleteWajibpajakListRequest): Promise<structs_ttypes.DeleteWajibpajakListResponse>;
deleteWajibPajakList(request: structs_ttypes.DeleteWajibpajakListRequest, callback?: (error: exceptions_ttypes.CoreServicesException, response: structs_ttypes.DeleteWajibpajakListResponse)=>void): void;
}
declare class Processor {
private _handler: object;
constructor(handler: object);
process(input: thrift.TProtocol, output: thrift.TProtocol): void;
process_login(seqid: number, input: thrift.TProtocol, output: thrift.TProtocol): void;
process_getProfile(seqid: number, input: thrift.TProtocol, output: thrift.TProtocol): void;
process_logout(seqid: number, input: thrift.TProtocol, output: thrift.TProtocol): void;
process_getUserList(seqid: number, input: thrift.TProtocol, output: thrift.TProtocol): void;
process_createUser(seqid: number, input: thrift.TProtocol, output: thrift.TProtocol): void;
process_deleteUsers(seqid: number, input: thrift.TProtocol, output: thrift.TProtocol): void;
process_getWajibPajakList(seqid: number, input: thrift.TProtocol, output: thrift.TProtocol): void;
process_createWajibPajak(seqid: number, input: thrift.TProtocol, output: thrift.TProtocol): void;
process_deleteWajibPajakList(seqid: number, input: thrift.TProtocol, output: thrift.TProtocol): void;
}

2027
frontend/gen/CoreService.js Normal file

File diff suppressed because it is too large Load diff

18
frontend/gen/exceptions_types.d.ts vendored Normal file
View file

@ -0,0 +1,18 @@
//
// Autogenerated by Thrift Compiler (0.16.0)
//
// DO NOT EDIT UNLESS YOU ARE SURE THAT YOU KNOW WHAT YOU ARE DOING
//
import thrift = require('thrift');
import Thrift = thrift.Thrift;
import Q = thrift.Q;
import Int64 = require('node-int64');
declare class CoreServicesException extends Thrift.TException {
public code: number;
public message: string;
public parameters: { [k: string]: string; };
constructor(args?: { code: number; message: string; parameters: { [k: string]: string; }; });
}

View file

@ -0,0 +1,114 @@
//
// Autogenerated by Thrift Compiler (0.16.0)
//
// DO NOT EDIT UNLESS YOU ARE SURE THAT YOU KNOW WHAT YOU ARE DOING
//
"use strict";
const thrift = require('thrift');
const Thrift = thrift.Thrift;
const Int64 = require('node-int64');
const ttypes = module.exports = {};
const CoreServicesException = module.exports.CoreServicesException = class extends Thrift.TException {
constructor(args) {
super(args);
this.name = "CoreServicesException";
this.code = null;
this.message = null;
this.parameters = null;
if (args) {
if (args.code !== undefined && args.code !== null) {
this.code = args.code;
}
if (args.message !== undefined && args.message !== null) {
this.message = args.message;
}
if (args.parameters !== undefined && args.parameters !== null) {
this.parameters = Thrift.copyMap(args.parameters, [null]);
}
}
}
read (input) {
input.readStructBegin();
while (true) {
const ret = input.readFieldBegin();
const ftype = ret.ftype;
const fid = ret.fid;
if (ftype == Thrift.Type.STOP) {
break;
}
switch (fid) {
case 1:
if (ftype == Thrift.Type.I32) {
this.code = input.readI32();
} else {
input.skip(ftype);
}
break;
case 2:
if (ftype == Thrift.Type.STRING) {
this.message = input.readString();
} else {
input.skip(ftype);
}
break;
case 3:
if (ftype == Thrift.Type.MAP) {
this.parameters = {};
const _rtmp31 = input.readMapBegin();
const _size0 = _rtmp31.size || 0;
for (let _i2 = 0; _i2 < _size0; ++_i2) {
let key3 = null;
let val4 = null;
key3 = input.readString();
val4 = input.readString();
this.parameters[key3] = val4;
}
input.readMapEnd();
} else {
input.skip(ftype);
}
break;
default:
input.skip(ftype);
}
input.readFieldEnd();
}
input.readStructEnd();
return;
}
write (output) {
output.writeStructBegin('CoreServicesException');
if (this.code !== null && this.code !== undefined) {
output.writeFieldBegin('code', Thrift.Type.I32, 1);
output.writeI32(this.code);
output.writeFieldEnd();
}
if (this.message !== null && this.message !== undefined) {
output.writeFieldBegin('message', Thrift.Type.STRING, 2);
output.writeString(this.message);
output.writeFieldEnd();
}
if (this.parameters !== null && this.parameters !== undefined) {
output.writeFieldBegin('parameters', Thrift.Type.MAP, 3);
output.writeMapBegin(Thrift.Type.STRING, Thrift.Type.STRING, Thrift.objectLength(this.parameters));
for (let kiter5 in this.parameters) {
if (this.parameters.hasOwnProperty(kiter5)) {
let viter6 = this.parameters[kiter5];
output.writeString(kiter5);
output.writeString(viter6);
}
}
output.writeMapEnd();
output.writeFieldEnd();
}
output.writeFieldStop();
output.writeStructEnd();
return;
}
};

18
frontend/gen/exceptionsc_types.d.ts vendored Normal file
View file

@ -0,0 +1,18 @@
//
// Autogenerated by Thrift Compiler (0.16.0)
//
// DO NOT EDIT UNLESS YOU ARE SURE THAT YOU KNOW WHAT YOU ARE DOING
//
import thrift = require('thrift');
import Thrift = thrift.Thrift;
import Q = thrift.Q;
import Int64 = require('node-int64');
declare class CommonException extends Thrift.TException {
public code: number;
public message: string;
public metadata?: { [k: string]: string; };
constructor(args?: { code: number; message: string; metadata?: { [k: string]: string; }; });
}

View file

@ -0,0 +1,114 @@
//
// Autogenerated by Thrift Compiler (0.16.0)
//
// DO NOT EDIT UNLESS YOU ARE SURE THAT YOU KNOW WHAT YOU ARE DOING
//
"use strict";
const thrift = require('thrift');
const Thrift = thrift.Thrift;
const Int64 = require('node-int64');
const ttypes = module.exports = {};
const CommonException = module.exports.CommonException = class extends Thrift.TException {
constructor(args) {
super(args);
this.name = "CommonException";
this.code = null;
this.message = null;
this.metadata = null;
if (args) {
if (args.code !== undefined && args.code !== null) {
this.code = args.code;
}
if (args.message !== undefined && args.message !== null) {
this.message = args.message;
}
if (args.metadata !== undefined && args.metadata !== null) {
this.metadata = Thrift.copyMap(args.metadata, [null]);
}
}
}
read (input) {
input.readStructBegin();
while (true) {
const ret = input.readFieldBegin();
const ftype = ret.ftype;
const fid = ret.fid;
if (ftype == Thrift.Type.STOP) {
break;
}
switch (fid) {
case 1:
if (ftype == Thrift.Type.I32) {
this.code = input.readI32();
} else {
input.skip(ftype);
}
break;
case 2:
if (ftype == Thrift.Type.STRING) {
this.message = input.readString();
} else {
input.skip(ftype);
}
break;
case 3:
if (ftype == Thrift.Type.MAP) {
this.metadata = {};
const _rtmp31 = input.readMapBegin();
const _size0 = _rtmp31.size || 0;
for (let _i2 = 0; _i2 < _size0; ++_i2) {
let key3 = null;
let val4 = null;
key3 = input.readString();
val4 = input.readString();
this.metadata[key3] = val4;
}
input.readMapEnd();
} else {
input.skip(ftype);
}
break;
default:
input.skip(ftype);
}
input.readFieldEnd();
}
input.readStructEnd();
return;
}
write (output) {
output.writeStructBegin('CommonException');
if (this.code !== null && this.code !== undefined) {
output.writeFieldBegin('code', Thrift.Type.I32, 1);
output.writeI32(this.code);
output.writeFieldEnd();
}
if (this.message !== null && this.message !== undefined) {
output.writeFieldBegin('message', Thrift.Type.STRING, 2);
output.writeString(this.message);
output.writeFieldEnd();
}
if (this.metadata !== null && this.metadata !== undefined) {
output.writeFieldBegin('metadata', Thrift.Type.MAP, 3);
output.writeMapBegin(Thrift.Type.STRING, Thrift.Type.STRING, Thrift.objectLength(this.metadata));
for (let kiter5 in this.metadata) {
if (this.metadata.hasOwnProperty(kiter5)) {
let viter6 = this.metadata[kiter5];
output.writeString(kiter5);
output.writeString(viter6);
}
}
output.writeMapEnd();
output.writeFieldEnd();
}
output.writeFieldStop();
output.writeStructEnd();
return;
}
};

13
frontend/gen/service_types.d.ts vendored Normal file
View file

@ -0,0 +1,13 @@
//
// Autogenerated by Thrift Compiler (0.16.0)
//
// DO NOT EDIT UNLESS YOU ARE SURE THAT YOU KNOW WHAT YOU ARE DOING
//
import thrift = require('thrift');
import Thrift = thrift.Thrift;
import Q = thrift.Q;
import Int64 = require('node-int64');
import structs_ttypes = require('./structs_types');
import exceptions_ttypes = require('./exceptions_types');

View file

@ -0,0 +1,16 @@
//
// Autogenerated by Thrift Compiler (0.16.0)
//
// DO NOT EDIT UNLESS YOU ARE SURE THAT YOU KNOW WHAT YOU ARE DOING
//
"use strict";
const thrift = require('thrift');
const Thrift = thrift.Thrift;
const Int64 = require('node-int64');
const structs_ttypes = require('./structs_types');
const exceptions_ttypes = require('./exceptions_types');
const ttypes = module.exports = {};

209
frontend/gen/structs_types.d.ts vendored Normal file
View file

@ -0,0 +1,209 @@
//
// Autogenerated by Thrift Compiler (0.16.0)
//
// DO NOT EDIT UNLESS YOU ARE SURE THAT YOU KNOW WHAT YOU ARE DOING
//
import thrift = require('thrift');
import Thrift = thrift.Thrift;
import Q = thrift.Q;
import Int64 = require('node-int64');
declare enum RoleType {
UNKNOWN = 0,
SYSTEM = 1,
USER = 2,
WP_OWNER = 100,
WP_ADMIN = 110,
}
declare enum JenisPajak {
UNKNOWN = 0,
PPH_21 = 1,
PPH_23 = 2,
PPH_25 = 3,
PPH_26 = 4,
PPH_4_2 = 5,
PPH_15 = 6,
PPN = 7,
TAHUNAN = 8,
}
declare enum WajibPajakOwnership {
UNKNOWN = 0,
OWNED = 1,
ROLE = 2,
}
declare class Role {
public id: Int64;
public displayName: string;
public roleType: RoleType;
public creator?: User;
public users?: User[];
public wajibPajakList?: WajibPajak[];
constructor(args?: { id: Int64; displayName: string; roleType: RoleType; creator?: User; users?: User[]; wajibPajakList?: WajibPajak[]; });
}
declare class User {
public id: Int64;
public username: string;
public password: string;
public displayName: string;
public privateKey: Buffer;
public publicKey: Buffer;
public roles?: Role[];
public ownedWajibPajakList?: WajibPajak[];
public rolesWajibPajakList?: Role[];
constructor(args?: { id: Int64; username: string; password: string; displayName: string; privateKey: Buffer; publicKey: Buffer; roles?: Role[]; ownedWajibPajakList?: WajibPajak[]; rolesWajibPajakList?: Role[]; });
}
declare class WajibPajakProfile {
public npwp: string;
public displayName: string;
public address: string;
constructor(args?: { npwp: string; displayName: string; address: string; });
}
declare class WajibPajakTaxObligation {
public id: Int64;
public obligation: JenisPajak;
public isActive: boolean;
constructor(args?: { id: Int64; obligation: JenisPajak; isActive: boolean; });
}
declare class WajibPajak {
public id: Int64;
public profile: WajibPajakProfile;
public owners: User[];
public taxObligations?: WajibPajakTaxObligation[];
public roles?: Role[];
constructor(args?: { id: Int64; profile: WajibPajakProfile; owners: User[]; taxObligations?: WajibPajakTaxObligation[]; roles?: Role[]; });
}
declare class Pagination {
public page: Int64;
public rowsPerPage: Int64;
constructor(args?: { page: Int64; rowsPerPage: Int64; });
}
declare class AlertInfo {
public title: string;
public description: string;
constructor(args?: { title: string; description: string; });
}
declare class LoginRequest {
public username: string;
public password: string;
constructor(args?: { username: string; password: string; });
}
declare class LoginResponse {
public trkToken: string;
public token: string;
public profile?: User;
constructor(args?: { trkToken: string; token: string; profile?: User; });
}
declare class LogoutRequest {
public token: string;
constructor(args?: { token: string; });
}
declare class LogoutResponse {
}
declare class GetProfileRequest {
}
declare class GetProfileResponse {
public profile: User;
constructor(args?: { profile: User; });
}
declare class GetUserListRequest {
public pagination: Pagination;
public searchTerm: string;
constructor(args?: { pagination: Pagination; searchTerm: string; });
}
declare class GetUserListResponse {
public pagination: Pagination;
public totalUsers: Int64;
public users: User[];
constructor(args?: { pagination: Pagination; totalUsers: Int64; users: User[]; });
}
declare class CreateUserRequest {
public user: User;
constructor(args?: { user: User; });
}
declare class CreateUserResponse {
}
declare class DeleteUsersRequest {
public userIds: Int64[];
constructor(args?: { userIds: Int64[]; });
}
declare class DeleteUsersResponse {
public success: Int64[];
public ignored: Int64[];
constructor(args?: { success: Int64[]; ignored: Int64[]; });
}
declare class GetWajibPajakListRequest {
public pagination: Pagination;
public ownership: WajibPajakOwnership;
public searchTerm: string;
constructor(args?: { pagination: Pagination; ownership: WajibPajakOwnership; searchTerm: string; });
}
declare class GetWajibPajakListResponse {
public pagination: Pagination;
public totalWajibPajak: Int64;
public wajibPajakList: WajibPajak[];
constructor(args?: { pagination: Pagination; totalWajibPajak: Int64; wajibPajakList: WajibPajak[]; });
}
declare class CreateWajibPajakRequest {
public wajibPajak: WajibPajak;
constructor(args?: { wajibPajak: WajibPajak; });
}
declare class CreateWajibPajakResponse {
}
declare class DeleteWajibpajakListRequest {
public wpIds: Int64[];
constructor(args?: { wpIds: Int64[]; });
}
declare class DeleteWajibpajakListResponse {
public success: Int64[];
public ignored: Int64[];
constructor(args?: { success: Int64[]; ignored: Int64[]; });
}

File diff suppressed because it is too large Load diff

13
frontend/index.html Normal file
View file

@ -0,0 +1,13 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<link rel="icon" type="image/svg+xml" href="/vite.svg" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>WPW-CTL</title>
</head>
<body>
<div id="app"></div>
<script type="module" src="/src/main.ts"></script>
</body>
</html>

95
frontend/package.json Normal file
View file

@ -0,0 +1,95 @@
{
"name": "frontend",
"private": true,
"version": "0.0.0",
"type": "module",
"scripts": {
"dev": "vite",
"build": "vue-tsc && vite build",
"typecheck": "vuedx-typecheck .",
"lint": "eslint --fix .",
"preview": "vite preview"
},
"dependencies": {
"@algolia/client-search": "^4.14.2",
"@heroicons/vue": "^2.0.13",
"@vueuse/core": "^9.4.0",
"classnames": "^2.3.2",
"flowbite": "1.5.0",
"flowbite-vue": "^0.0.6",
"pinia": "^2.0.23",
"thrift": "^0.16.0",
"vue": "^3.2.41",
"vue-router": "^4.1.6",
"vue3-easy-data-table": "^1.5.12",
"vue3-perfect-scrollbar": "^1.6.1"
},
"devDependencies": {
"node-stdlib-browser": "^1.2.0",
"@ampproject/rollup-plugin-closure-compiler": "^0.27.0",
"@babel/core": "^7.0.0",
"@babel/plugin-syntax-class-properties": "^7.12.13",
"@babel/plugin-transform-classes": "^7.19.0",
"@babel/preset-env": "^7.19.4",
"@esbuild-plugins/node-globals-polyfill": "^0.1.1",
"@esbuild-plugins/node-modules-polyfill": "^0.1.4",
"@lopatnov/rollup-plugin-uglify": "^2.1.5",
"@originjs/vite-plugin-commonjs": "^1.0.3",
"@rollup/plugin-babel": "^6.0.2",
"@rollup/plugin-commonjs": "^22.0.2",
"@rollup/plugin-inject": "^4.0.4",
"@rollup/plugin-node-resolve": "^13.3.0",
"@rollup/plugin-terser": "^0.1.0",
"@rushstack/eslint-patch": "^1.2.0",
"@types/node": "^16.18.3",
"@types/thrift": "^0.10.11",
"@vitejs/plugin-legacy": "^2.3.0",
"@vitejs/plugin-vue": "^3.2.0",
"@vitejs/plugin-vue-jsx": "^2.1.0",
"@vue/eslint-config-prettier": "^7.0.0",
"@vue/eslint-config-typescript": "^11.0.2",
"@vue/tsconfig": "^0.1.3",
"@vuedx/typecheck": "^0.7.6",
"@vuedx/typescript-plugin-vue": "^0.7.6",
"autoprefixer": "^10.4.13",
"babel-plugin-minify-dead-code-elimination": "^0.5.2",
"class-names": "^1.0.0",
"esbuild": "*",
"eslint": "^8.5.0",
"eslint-config-prettier": "^8.5.0",
"eslint-plugin-prettier": "^4.2.1",
"eslint-plugin-vue": "^9.7.0",
"node-int64": "^0.4.0",
"postcss": "^8.4.18",
"prettier": "^2.7.1",
"rollup": "^2.79.0",
"rollup-obfuscator": "^3.0.1",
"rollup-plugin-node-globals": "^1.4.0",
"rollup-plugin-node-polyfills": "^0.2.1",
"tailwindcss": "^3.2.2",
"terser": "^5.15.1",
"typescript": "^4.6.4",
"vite": "^3.2.0",
"vite-plugin-chunk-split": "^0.4.3",
"vite-plugin-node-stdlib-browser": "^0.1.1",
"vue-tsc": "^1.0.9"
},
"eslintConfigxxx": {
"root": true,
"env": {
"browser": true,
"node": true,
"es6": true
},
"extendsxx": [
"plugin:vue/essential",
"plugin:prettier/recommended",
"eslint:recommended"
],
"parserOptions": {
"parser": "babel-eslint"
},
"rules": {}
}
}

6337
frontend/pnpm-lock.yaml Normal file

File diff suppressed because it is too large Load diff

View file

@ -0,0 +1,6 @@
module.exports = {
plugins: {
tailwindcss: {},
autoprefixer: {},
},
};

1
frontend/public/vite.svg Normal file
View file

@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" aria-hidden="true" role="img" class="iconify iconify--logos" width="31.88" height="32" preserveAspectRatio="xMidYMid meet" viewBox="0 0 256 257"><defs><linearGradient id="IconifyId1813088fe1fbc01fb466" x1="-.828%" x2="57.636%" y1="7.652%" y2="78.411%"><stop offset="0%" stop-color="#41D1FF"></stop><stop offset="100%" stop-color="#BD34FE"></stop></linearGradient><linearGradient id="IconifyId1813088fe1fbc01fb467" x1="43.376%" x2="50.316%" y1="2.242%" y2="89.03%"><stop offset="0%" stop-color="#FFEA83"></stop><stop offset="8.333%" stop-color="#FFDD35"></stop><stop offset="100%" stop-color="#FFA800"></stop></linearGradient></defs><path fill="url(#IconifyId1813088fe1fbc01fb466)" d="M255.153 37.938L134.897 252.976c-2.483 4.44-8.862 4.466-11.382.048L.875 37.958c-2.746-4.814 1.371-10.646 6.827-9.67l120.385 21.517a6.537 6.537 0 0 0 2.322-.004l117.867-21.483c5.438-.991 9.574 4.796 6.877 9.62Z"></path><path fill="url(#IconifyId1813088fe1fbc01fb467)" d="M185.432.063L96.44 17.501a3.268 3.268 0 0 0-2.634 3.014l-5.474 92.456a3.268 3.268 0 0 0 3.997 3.378l24.777-5.718c2.318-.535 4.413 1.507 3.936 3.838l-7.361 36.047c-.495 2.426 1.782 4.5 4.151 3.78l15.304-4.649c2.372-.72 4.652 1.36 4.15 3.788l-11.698 56.621c-.732 3.542 3.979 5.473 5.943 2.437l1.313-2.028l72.516-144.72c1.215-2.423-.88-5.186-3.54-4.672l-25.505 4.922c-2.396.462-4.435-1.77-3.759-4.114l16.646-57.705c.677-2.35-1.37-4.583-3.769-4.113Z"></path></svg>

After

Width:  |  Height:  |  Size: 1.5 KiB

77
frontend/src/App.vue Normal file
View file

@ -0,0 +1,77 @@
<script setup lang="ts">
// This starter template is using Vue 3 <script setup> SFCs
// Check out https://vuejs.org/api/sfc-script-setup.html#script-setup
import NavigationBar from './components/NavigationBar.vue';
import { useHub } from './hub';
import { useTheme } from './stores/theme';
import /* * as */structs from '@thriftgen/structs_types';
import /* * as */exceptions from '@thriftgen/exceptions_types';
import { RouteMeta, useRouter } from 'vue-router';
import { AlertType, useAlert } from './stores/alert';
import { useAuth } from './stores/auth';
import { watch } from 'vue';
import { storeToRefs } from 'pinia';
const authRef = storeToRefs(useAuth());
const hub = useHub();
const theme = useTheme();
const themeRef = storeToRefs(theme);
const router = useRouter();
async function fetchProfile() {
const alert = useAlert();
const auth = useAuth();
try {
const resp = await hub.BizCore.client.getProfile(new structs.GetProfileRequest());
console.log('get profile', resp);
auth.setProfile(resp.profile);
return true;
} catch (e) {
console.error('get profile', e);
if (e instanceof exceptions.CoreServicesException) {
alert.showContent('Attention', 'Login first', { type: AlertType.Error });
}
}
return false;
}
router.beforeResolve(async (to) => {
// inherit navigation bar visibility from route meta
theme.setNavigationBarVisibility(
to.meta.displayNavigationBar == undefined ? true : to.meta.displayNavigationBar,
);
// fetch user profile
const validProfile = await fetchProfile();
if (to.meta.requiresAuth) {
if (!validProfile) return '/login';
}
return;
});
// monitor state change of logged in
watch(authRef.isLoggedIn, (loggedIn) => {
const meta = router.currentRoute.value.meta;
if (meta.requiresAuth && !loggedIn) {
router.push('/login');
}
});
// monitor state change of darh theme
watch(themeRef.isDarkThemeEnabled, (darkTheme) => {
if (darkTheme) {
document.documentElement.classList.add('dark-theme');
} else {
document.documentElement.classList.remove('dark-theme');
}
});
</script>
<template>
<NavigationBar v-if="theme.navigationBarVisibility" />
<RouterView />
</template>
<style scoped>
/* */
</style>

View file

@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" aria-hidden="true" role="img" class="iconify iconify--logos" width="37.07" height="36" preserveAspectRatio="xMidYMid meet" viewBox="0 0 256 198"><path fill="#41B883" d="M204.8 0H256L128 220.8L0 0h97.92L128 51.2L157.44 0h47.36Z"></path><path fill="#41B883" d="m0 0l128 220.8L256 0h-51.2L128 132.48L50.56 0H0Z"></path><path fill="#35495E" d="M50.56 0L128 133.12L204.8 0h-47.36L128 51.2L97.92 0H50.56Z"></path></svg>

After

Width:  |  Height:  |  Size: 496 B

View file

@ -0,0 +1,53 @@
<script lang="ts" setup>
import { useAlert, AlertType } from '@/stores/alert';
const data = useAlert();
function hideAlert() {
data.hide();
}
</script>
<template>
<div
v-if="data.show"
:class="[
(() => {
switch (data.type) {
case AlertType.Error:
return 'text-red-700 bg-red-100 dark:bg-red-200 dark:text-red-800';
case AlertType.Warning:
return 'text-yellow-700 bg-yellow-100 dark:bg-yellow-200 dark:text-yellow-800';
case AlertType.Success:
return 'text-green-700 bg-green-100 dark:bg-green-200 dark:text-green-800';
}
return '';
})(),
'flex p-4 mb-6 text-sm rounded-lg',
data.className,
]"
role="alert"
>
<span class="font-medium">{{ data.title }}:&nbsp;</span>
{{ data.description }}
<button
type="button"
@click="hideAlert"
class="ml-auto -mx-1.5 -my-1.5 rounded-lg focus:ring-2 p-1.5 inline-flex h-8 w-8"
>
<span class="sr-only">Close</span>
<svg
aria-hidden="true"
class="w-5 h-5"
fill="currentColor"
viewBox="0 0 20 20"
xmlns="http://www.w3.org/2000/svg"
>
<path
fill-rule="evenodd"
d="M4.293 4.293a1 1 0 011.414 0L10 8.586l4.293-4.293a1 1 0 111.414 1.414L11.414 10l4.293 4.293a1 1 0 01-1.414 1.414L10 11.414l-4.293 4.293a1 1 0 01-1.414-1.414L8.586 10 4.293 5.707a1 1 0 010-1.414z"
clip-rule="evenodd"
></path>
</svg>
</button>
</div>
</template>

View file

@ -0,0 +1,144 @@
<script lang="ts" setup>
import { useHub } from '@/hub';
import { useAlert } from '@/stores/alert';
import { useAuth } from '@/stores/auth';
import { computed, ref, watch } from 'vue';
import { RouteLocationRaw, RouterLink, useRouter } from 'vue-router';
import /* * as */structs from '@thriftgen/structs_types';
import SpinnerIcon from './SpinnerIcon.vue';
import { NavbarLinkMeta } from '@/router';
const auth = useAuth();
const router = useRouter();
const alert = useAlert();
const hub = useHub();
const isOnLoginPage = computed(() => router.currentRoute.value.path === '/login');
const isLoading = ref(false);
async function authCtl() {
isLoading.value = true;
if (auth.isLoggedIn) {
try {
const req = new structs.LogoutRequest({ token: auth.authToken });
const resp = await hub.BizCore.client.logout(req);
console.log('logout', resp);
auth.logout();
alert.showContent('Attention', 'Logged out');
// router.push('/login');
} catch (e) {
console.error('logout', e);
} finally {
isLoading.value = false;
}
} else {
isLoading.value = false;
router.push('/login');
}
}
const navLink = computed(() => {
const qs = router
.getRoutes()
.filter((x) => x.meta.navbarLink)
.filter((x) => !x.meta.requiresAuth || (x.meta.requiresAuth && auth.isLoggedIn))
.map((x) => ({ to: x.path, ...x.meta.navbarLink }))
.sort((x, y) => (y.priority || 0) - (x.priority || 0));
return qs;
});
</script>
<template>
<nav class="bg-white border-gray-200 px-2 sm:px-4 py-2.5 rounded dark:bg-gray-900">
<div class="container flex flex-wrap justify-between items-center mx-auto">
<a href="/" class="flex items-center">
<img
src="https://flowbite.com/docs/images/logo.svg"
class="mr-3 h-6 sm:h-9"
alt="Flowbite Logo"
/>
<span class="self-center text-xl font-semibold whitespace-nowrap dark:text-white"
>WPW-CTL</span
>
</a>
<button
data-collapse-toggle="navbar-default"
type="button"
class="inline-flex items-center p-2 ml-3 text-sm text-gray-500 rounded-lg md:hidden hover:bg-gray-100 focus:outline-none focus:ring-2 focus:ring-gray-200 dark:text-gray-400 dark:hover:bg-gray-700 dark:focus:ring-gray-600"
aria-controls="navbar-default"
aria-expanded="false"
>
<span class="sr-only">Open main menu</span>
<svg
class="w-6 h-6"
aria-hidden="true"
fill="currentColor"
viewBox="0 0 20 20"
xmlns="http://www.w3.org/2000/svg"
>
<path
fill-rule="evenodd"
d="M3 5a1 1 0 011-1h12a1 1 0 110 2H4a1 1 0 01-1-1zM3 10a1 1 0 011-1h12a1 1 0 110 2H4a1 1 0 01-1-1zM3 15a1 1 0 011-1h12a1 1 0 110 2H4a1 1 0 01-1-1z"
clip-rule="evenodd"
></path>
</svg>
</button>
<div class="hidden w-full md:block md:w-auto" id="navbar-default">
<ul
class="flex flex-col p-4 mt-4 bg-gray-50 rounded-lg border border-gray-100 md:flex-row md:space-x-8 md:mt-0 md:text-sm md:font-medium md:border-0 md:bg-white dark:bg-gray-800 md:dark:bg-gray-900 dark:border-gray-700"
>
<li v-if="auth.isLoggedIn && auth.profile">
{{ auth.profile?.displayName }} ({{ auth.profile?.username }})
</li>
<li v-for="(link, i) in navLink" :key="i">
<RouterLink :to="link.to" :class="link.classList" aria-current="date">
{{ link.caption }}
</RouterLink>
</li>
<li>
<RouterLink
to="#"
v-if="!isOnLoginPage"
@click="authCtl"
class="block py-2 pr-4 pl-3 text-white bg-blue-700 rounded md:bg-transparent md:text-blue-700 md:p-0 dark:text-white"
aria-current="page"
>
<SpinnerIcon class="text-blue" v-if="isLoading" />
{{ auth.isLoggedIn ? 'Logout' : 'Login' }}</RouterLink
>
</li>
<!-- <li>
<a
href="#"
class="block py-2 pr-4 pl-3 text-gray-700 rounded hover:bg-gray-100 md:hover:bg-transparent md:border-0 md:hover:text-blue-700 md:p-0 dark:text-gray-400 md:dark:hover:text-white dark:hover:bg-gray-700 dark:hover:text-white md:dark:hover:bg-transparent"
>About</a
>
</li>
<li>
<a
href="#"
class="block py-2 pr-4 pl-3 text-gray-700 rounded hover:bg-gray-100 md:hover:bg-transparent md:border-0 md:hover:text-blue-700 md:p-0 dark:text-gray-400 md:dark:hover:text-white dark:hover:bg-gray-700 dark:hover:text-white md:dark:hover:bg-transparent"
>Services</a
>
</li>
<li>
<a
href="#"
class="block py-2 pr-4 pl-3 text-gray-700 rounded hover:bg-gray-100 md:hover:bg-transparent md:border-0 md:hover:text-blue-700 md:p-0 dark:text-gray-400 md:dark:hover:text-white dark:hover:bg-gray-700 dark:hover:text-white md:dark:hover:bg-transparent"
>Pricing</a
>
</li>
<li>
<a
href="#"
class="block py-2 pr-4 pl-3 text-gray-700 rounded hover:bg-gray-100 md:hover:bg-transparent md:border-0 md:hover:text-blue-700 md:p-0 dark:text-gray-400 md:dark:hover:text-white dark:hover:bg-gray-700 dark:hover:text-white md:dark:hover:bg-transparent"
>Contact</a
>
</li> -->
</ul>
</div>
</div>
</nav>
</template>

View file

@ -0,0 +1,19 @@
<script lang="ts" setup></script>
<template>
<svg
role="status"
class="inline mr-3 w-4 h-4 animate-spin"
viewBox="0 0 100 101"
fill="none"
xmlns="http://www.w3.org/2000/svg"
>
<path
d="M100 50.5908C100 78.2051 77.6142 100.591 50 100.591C22.3858 100.591 0 78.2051 0 50.5908C0 22.9766 22.3858 0.59082 50 0.59082C77.6142 0.59082 100 22.9766 100 50.5908ZM9.08144 50.5908C9.08144 73.1895 27.4013 91.5094 50 91.5094C72.5987 91.5094 90.9186 73.1895 90.9186 50.5908C90.9186 27.9921 72.5987 9.67226 50 9.67226C27.4013 9.67226 9.08144 27.9921 9.08144 50.5908Z"
fill="#E5E7EB"
/>
<path
d="M93.9676 39.0409C96.393 38.4038 97.8624 35.9116 97.0079 33.5539C95.2932 28.8227 92.871 24.3692 89.8167 20.348C85.8452 15.1192 80.8826 10.7238 75.2124 7.41289C69.5422 4.10194 63.2754 1.94025 56.7698 1.05124C51.7666 0.367541 46.6976 0.446843 41.7345 1.27873C39.2613 1.69328 37.813 4.19778 38.4501 6.62326C39.0873 9.04874 41.5694 10.4717 44.0505 10.1071C47.8511 9.54855 51.7191 9.52689 55.5402 10.0491C60.8642 10.7766 65.9928 12.5457 70.6331 15.2552C75.2735 17.9648 79.3347 21.5619 82.5849 25.841C84.9175 28.9121 86.7997 32.2913 88.1811 35.8758C89.083 38.2158 91.5421 39.6781 93.9676 39.0409Z"
fill="currentColor"
/>
</svg>
</template>

View file

@ -0,0 +1,43 @@
import { HttpHeaders, TBinaryProtocol, TCompactProtocol, TJSONProtocol } from 'thrift';
import hub, { ClientOptions } from '.';
import { ServiceType } from './service_type';
import { HubHeader } from './hub';
let defaultHubUrl: URL;
let hubSolveMethod = 'UNRESOLVED';
try {
defaultHubUrl = new URL(import.meta.env.VITE_HUB_URL || window.location.href);
hubSolveMethod = import.meta.env.VITE_HUB_URL ? 'ENV' : 'BROWSER';
} catch {
defaultHubUrl = new URL(window.location.href);
hubSolveMethod = 'BROWSER2';
}
console.log(`[hub] url ${defaultHubUrl} ${hubSolveMethod}`);
export const defaultHost = defaultHubUrl.hostname;
export const defaultPort =
parseInt(defaultHubUrl.port) || (window.location.protocol === 'https:' ? 443 : 80);
export const defaultConfig: ClientOptions = {
host: defaultHost,
port: defaultPort,
https: defaultHubUrl.protocol.indexOf('https') !== -1,
path: defaultHubUrl.pathname + defaultHubUrl.search,
protocol: TCompactProtocol,
headers: ((): HttpHeaders => {
const headers = {
...HubHeader,
'Content-Type': 'application/x-thrift; charset=utf-8; protocol=TCOMPACT',
};
return headers;
})(),
};
console.log(`[hub] default config:`, defaultConfig);
export const defaultClientOptions = new Map<ServiceType, ClientOptions>([
[
ServiceType.BizCore,
{
...defaultConfig,
},
],
]);

36
frontend/src/hub/hub.ts Normal file
View file

@ -0,0 +1,36 @@
import { ThriftClient } from './xhr_client';
import /* * as */Core from '@thriftgen/CoreService';
import { ServiceType } from './service_type';
import { ClientOptions } from '.';
import { defaultClientOptions } from './defaults';
export const HubClientVersion = '1.0.0';
export const HeaderKeyHubVersion = 'x-hub-version';
export const HeaderKeyHubEnv = 'x-hub-env';
export const HeaderKeyHubApp = 'x-hub-app';
export const CookieKeyTrackToken = 'trk_token';
export const HeaderKeyTrackToken = 'x-hub-track-token';
export const CookieKeyAuthToken = 'auth_token';
export const HeaderKeyAuthToken = 'x-hub-auth-token';
export const HubHeader = {
[HeaderKeyHubVersion]: HubClientVersion,
[HeaderKeyHubEnv]: '*',
[HeaderKeyHubApp]: 'global',
};
export function defaultHubInit(
opts: Map<ServiceType, ClientOptions> = defaultClientOptions,
): ThriftHub {
console.log('[hub] init');
return new ThriftHub(opts);
}
export class ThriftHub {
BizCore: ThriftClient<Core.Client>;
constructor(opts: Map<ServiceType, ClientOptions>) {
this.BizCore = new ThriftClient(Core.Client, opts.get(ServiceType.BizCore)!);
}
}

28
frontend/src/hub/index.ts Normal file
View file

@ -0,0 +1,28 @@
import {
createXHRClient,
createXHRConnection,
TBinaryProtocol,
TJSONProtocol,
XHRConnection,
type ConnectOptions,
} from 'thrift';
import { App, inject } from 'vue';
import { defaultClientOptions } from './defaults';
import { defaultHubInit, ThriftHub } from './hub';
import { ServiceType } from './service_type';
export type ClientOptions = ConnectOptions & {
host: string;
port: number;
};
const hubProvideKey = 'thrift_hub';
export function useHub(): ThriftHub {
return inject(hubProvideKey) || defaultHubInit();
}
export default {
install: (app: App, opts: Map<ServiceType, ClientOptions> = defaultClientOptions) => {
app.provide(hubProvideKey, defaultHubInit(opts));
},
};

View file

@ -0,0 +1,3 @@
export enum ServiceType {
BizCore = 'biz_core',
}

View file

@ -0,0 +1,80 @@
import { getCookie, setCookie } from '@/utils/cookie';
import {
createClient,
createXHRClient,
createXHRConnection,
TClientConstructor,
XHRConnection,
} from 'thrift';
import { computed } from 'vue';
import { ClientOptions } from '.';
import { CookieKeyAuthToken, CookieKeyTrackToken, HeaderKeyAuthToken, HeaderKeyTrackToken } from './hub';
export function initXHRClient<TClient>(
clientCtor: TClientConstructor<TClient>,
opts: ClientOptions,
): TClient {
const { host, port, ...addOpts } = opts;
const conn = createXHRConnection(host, port, { ...addOpts });
const client = createXHRClient(clientCtor, conn);
return client;
}
export class ThriftClient<TClient> {
opts: ClientOptions;
conn: XHRConnection;
client: TClient;
constructor(clientCtor: TClientConstructor<TClient>, opts: ClientOptions) {
this.opts = opts;
this.conn = createXHRConnection(this.opts.host, this.opts.port, {
...this.opts,
});
const originalGetXmlHttpRequestObject = this.conn.getXmlHttpRequestObject;
this.conn.getXmlHttpRequestObject = () => {
const xreq = originalGetXmlHttpRequestObject();
xreq.responseType = 'arraybuffer';
return xreq;
};
const originalFlush = this.conn.flush;
this.conn.flush = () => {
const xreq = this.conn.getXmlHttpRequestObject();
xreq.overrideMimeType('application/x-thrift');
xreq.onreadystatechange = () => {
if (xreq.readyState == 4 && xreq.status == 200) {
// if (xreq.getAllResponseHeaders().indexOf(HeaderKeyTrackToken) >= 0) {
// const token = xreq.getResponseHeader(HeaderKeyTrackToken);
// if (token) {
// console.log('got trk token:', token);
// setCookie(CookieKeyTrackToken, token);
// }
// }
this.conn.setRecvBuffer(xreq.response);
}
};
const self: any = this.conn;
xreq.open('POST', self.url, true);
Object.keys(this.conn.headers).forEach(function (headerKey) {
xreq.setRequestHeader(headerKey, self.headers[headerKey]);
});
const trkToken = getCookie(CookieKeyTrackToken);
if (trkToken) {
console.log('use track token:', trkToken);
xreq.setRequestHeader(HeaderKeyTrackToken, trkToken);
}
const authToken = getCookie(CookieKeyAuthToken);
if (authToken) {
console.log('use auth token:', authToken);
xreq.setRequestHeader(HeaderKeyAuthToken, authToken);
}
try {
xreq.send(this.conn.send_buf);
} catch (e) {
console.error('xhr client:', e);
}
};
this.client = createXHRClient(clientCtor, this.conn);
}
}

25
frontend/src/main.ts Normal file
View file

@ -0,0 +1,25 @@
import 'vite/modulepreload-polyfill';
import { createApp } from 'vue';
import { createPinia } from 'pinia';
import './style.css';
import App from './App.vue';
import hub from '@/hub';
import router from './router';
import Vue3PerfectScrollbar from 'vue3-perfect-scrollbar';
import Vue3EasyDataTable from 'vue3-easy-data-table';
import 'vue3-easy-data-table/dist/style.css';
import 'flowbite';
// import 'flowbite-vue';
const app = createApp(App);
app.use(hub); // client hub
app.use(router);
app.use(createPinia());
app.use(Vue3PerfectScrollbar);
app.component('EasyDataTable', Vue3EasyDataTable);
app.mount('#app');

View file

@ -0,0 +1,24 @@
import core from '@/views/core';
import error from '@/views/error';
import { createRouter, createWebHistory } from 'vue-router';
export type NavbarLinkMeta = {
caption: string;
classList?: string;
priority?: number;
};
declare module 'vue-router' {
interface RouteMeta {
requiresAuth?: boolean;
displayNavigationBar?: boolean;
navbarLink?: NavbarLinkMeta;
}
}
const router = createRouter({
history: createWebHistory(),
routes: [...core.routes(), ...error.routes()],
});
export default router;

View file

@ -0,0 +1,55 @@
import { defineStore } from 'pinia';
export enum AlertType {
Error,
Warning,
Success,
}
export type Alert = {
show: boolean;
className: string;
title: string;
description: string;
type: AlertType;
};
const defaultData = () =>
<Alert>{
show: false,
className: '',
title: '',
description: '',
type: AlertType.Error,
};
export const useAlert = defineStore({
id: 'alert',
state: () => {
return defaultData();
},
getters: {},
actions: {
hide() {
this.show = false;
},
showContent(
title: string,
description: string,
opt?: { className?: string; type?: AlertType },
) {
Object.assign(this, defaultData());
this.title = title;
this.description = description;
this.show = true;
if (opt) {
if (opt.className) {
this.className = opt.className;
}
if (opt.type) {
this.type = opt.type;
}
}
},
},
});

View file

@ -0,0 +1,73 @@
import { defineStore } from 'pinia';
import /* * as */structs from '@thriftgen/structs_types';
import { deleteCookie, getCookie, setCookie } from '@/utils/cookie';
import { CookieKeyAuthToken, CookieKeyTrackToken } from '@/hub/hub';
import { RemovableRef, useLocalStorage } from '@vueuse/core';
export type Auth = {
trkToken: string;
token: string;
profile?: structs.User;
};
type AuthState = {
trkToken: RemovableRef<string>;
token: RemovableRef<string>;
profile?: structs.User;
};
export function isValidToken(token: string): boolean {
return token != '' && token != undefined && token != 'undefined';
}
export const useAuth = defineStore({
id: 'user',
state: () =>
<AuthState>{
trkToken: useLocalStorage('trkToken', ''),
token: useLocalStorage('token', ''),
profile: undefined,
},
getters: {
hasProfile(state): boolean {
return state.profile != null;
},
hasToken(state): boolean {
return isValidToken(this.trackToken);
},
isLoggedIn(state): boolean {
return this.hasToken && this.hasProfile;
},
trackToken(state): string {
return state.trkToken;
},
authToken(state): string {
return state.token;
},
},
actions: {
setTrackToken(token?: string) {
if (!token) return;
console.log('set track token:', token);
setCookie(CookieKeyTrackToken, token);
this.trkToken = token;
},
setAuthToken(token?: string) {
if (!token) return;
console.log('set auth token:', token);
setCookie(CookieKeyAuthToken, token);
this.token = token;
},
setProfile(profile: structs.User) {
this.profile = profile;
},
logout() {
deleteCookie(CookieKeyAuthToken);
deleteCookie(CookieKeyTrackToken);
this.profile = undefined;
this.token = '';
this.trkToken = '';
},
},
});

View file

@ -0,0 +1,25 @@
import { defineStore } from 'pinia';
export const useTheme = defineStore({
id: 'theme',
state: () => ({
_darkTheme: false,
_navigationBarVisibility: true,
}),
getters: {
isDarkThemeEnabled(state): boolean {
return state._darkTheme;
},
navigationBarVisibility(state): boolean {
return state._navigationBarVisibility
},
},
actions: {
toggleDarkTheme() {
this._darkTheme = !this._darkTheme;
},
setNavigationBarVisibility(visibility: boolean): void {
this._navigationBarVisibility = visibility;
},
},
});

4
frontend/src/style.css Normal file
View file

@ -0,0 +1,4 @@
@tailwind base;
@tailwind components;
@tailwind utilities;

View file

@ -0,0 +1,33 @@
/*
* General utils for managing cookies in Typescript.
*/
export function setCookie(name: string, val: string) {
const date = new Date();
const value = val;
// Set it expire in 7 days
date.setTime(date.getTime() + 7 * 24 * 60 * 60 * 1000);
// Set it
document.cookie = name + '=' + value + '; expires=' + date.toUTCString() + '; path=/';
}
export function getCookie(name: string): string | undefined {
const value = '; ' + document.cookie;
const parts = value.split('; ' + name + '=');
if (parts.length == 2) {
return parts.pop()?.split(';').shift();
}
return undefined;
}
export function deleteCookie(name: string) {
const date = new Date();
// Set it expire in -1 days
date.setTime(date.getTime() + -1 * 24 * 60 * 60 * 1000);
// Set it
document.cookie = name + '=; expires=' + date.toUTCString() + '; path=/';
}

View file

@ -0,0 +1,501 @@
<script lang="ts" setup>
import /* * as */structs from '@thriftgen/structs_types';
import /* * as */exceptions from '@thriftgen/exceptions_types';
import { useHub } from '@/hub';
import { computed, ref, watch } from 'vue';
import type { Header, Item, ServerOptions, SortType } from 'vue3-easy-data-table';
import HeadingPage from './components/HeadingPage.vue';
import ModalDialog from './components/ModalDialog.vue';
import AlertBox from '@/components/AlertBox.vue';
import { AlertType, useAlert } from '@/stores/alert';
import { EyeIcon, EyeSlashIcon } from '@heroicons/vue/24/solid';
import SpinnerIcon from '@/components/SpinnerIcon.vue';
const isLoading = ref(false);
const hub = useHub();
const alert = useAlert();
const sortBy: string[] = ['id', 'username'];
const sortType: SortType[] = ['desc', 'asc'];
const headers: Header[] = [
// { text: '#', value: 'id', sortable: false },
{ text: 'Username', value: 'username', sortable: false },
{ text: 'Name', value: 'displayName' },
{ text: 'Roles', value: 'roles' },
{ text: 'Actions', value: 'actions' },
];
const items = ref<Item[]>([]);
const searchTerm = ref('');
const serverItemsLength = ref(0);
const serverOptions = ref<ServerOptions>({
page: 1,
rowsPerPage: 25,
});
const itemsSelected = ref<Item[]>([]);
const hasSelectedItems = computed(() => itemsSelected.value.length > 0);
const captionBulk = computed(() =>
hasSelectedItems.value ? ` (${itemsSelected.value.length})` : '',
);
const searchField = ['Username', 'Name', 'Roles'];
async function fetchUsersToTable() {
isLoading.value = true;
try {
console.log('svr opt:', serverOptions);
const { page, rowsPerPage } = serverOptions.value;
const pagination = new structs.Pagination({ page, rowsPerPage });
const req = new structs.GetUserListRequest({
pagination: pagination,
searchTerm: searchTerm.value,
});
const res = await hub.BizCore.client.getUserList(req);
console.log('get users', res);
items.value = res.users;
serverItemsLength.value = res.totalUsers + 0;
} catch (e: any) {
if (e instanceof exceptions.CoreServicesException) {
const alertVal = e.parameters['alert'];
if (alertVal) {
const alertData: structs.AlertInfo = JSON.parse(alertVal);
alert.showContent(alertData.title, alertData.description);
return;
}
}
alert.showContent('Server Error', e.toString());
} finally {
isLoading.value = false;
}
}
async function deleteUser(user: structs.User) {
console.log('delete', user);
const users = [user];
const userConfirmed = await showDeleteDialog(users);
if (!userConfirmed) {
return;
}
await doDeleteUsers(users);
await fetchUsersToTable();
return;
}
async function manageRoles(user: structs.User) {
console.log('manage role', user);
return;
}
async function createUser() {
console.log('create user');
showCreateUserDialog(async (req) => {
try {
const resp = await hub.BizCore.client.createUser(req);
console.log('create user resp', resp);
} catch (e: any) {
if (e instanceof exceptions.CoreServicesException) {
const alertVal = e.parameters['alert'];
if (alertVal) {
const alertData: structs.AlertInfo = JSON.parse(alertVal);
alert.showContent(alertData.title, alertData.description);
return;
}
}
alert.showContent('Server Error', e.toString());
} finally {
hideDialog();
fetchUsersToTable();
}
});
}
async function deleteUserSelected() {
if (!hasSelectedItems.value) {
return;
}
const users = itemsSelected.value as structs.User[];
const userConfirmed = await showDeleteDialog(users);
if (!userConfirmed) {
return;
}
await doDeleteUsers(users);
await fetchUsersToTable();
itemsSelected.value = [];
return;
}
async function manageRolesSelected() {
if (!hasSelectedItems.value) {
return;
}
console.log('selected ', itemsSelected.value.length);
return;
}
// initial load
fetchUsersToTable();
watch(serverOptions, () => {
fetchUsersToTable();
});
watch(searchTerm, (value) => {
fetchUsersToTable();
});
/* RPC calls */
async function doDeleteUsers(users: structs.User[]) {
const req = new structs.DeleteUsersRequest({
userIds: users.map((u) => u.id).filter((id) => id),
});
try {
const resp = await hub.BizCore.client.deleteUsers(req);
console.log('delete user result', resp);
const { success: deleted, ignored } = resp;
alert.showContent(
'Delete user',
`${deleted.length > 0 ? deleted.length + ' deleted' + (ignored.length > 0 ? ', ' : '') : ''}${
ignored.length > 0 ? ignored.length + ' ignored' : ''
}.`,
{
type: AlertType.Success,
},
);
} catch (e: any) {
if (e instanceof exceptions.CoreServicesException) {
const alertVal = e.parameters['alert'];
if (alertVal) {
const alertData: structs.AlertInfo = JSON.parse(alertVal);
alert.showContent(alertData.title, alertData.description);
return;
}
}
alert.showContent('Server Error', e.toString());
}
}
/* Modal script */
const modalView = ref('');
const modalContext = ref({} as any);
const modalActionCb = ref<(() => void)[]>([]);
const modalWithClose = ref(false);
const isModalShow = ref(false);
function showDialog(
view: string,
ctx: any,
opt?: {
withClose?: boolean;
actions?: ((resolve: (value: boolean | PromiseLike<boolean>) => void) => void)[];
},
): Promise<boolean> {
isModalShow.value = true;
modalWithClose.value = (opt || {}).withClose || false;
modalView.value = view;
modalContext.value = ctx;
return new Promise((resolve) => {
const actions = ((opt || {}).actions || []).map((act) => () => {
isModalShow.value = false; // TODO: make this optional
act(resolve);
});
modalActionCb.value = actions;
});
}
function hideDialog() {
isModalShow.value = false;
}
function showDeleteDialog(users: structs.User[]): Promise<boolean> {
return showDialog('confirm-delete', users, {
withClose: false,
actions: [
(resolve) => {
resolve(true);
},
(resolve) => {
resolve(false);
},
],
});
}
function showCreateUserDialog(
actionCb: (req: structs.CreateUserRequest) => Promise<void>,
): Promise<boolean> {
const state = { passwordShow: ref(false), isLoading: ref(false) };
const data = {
username: ref(''),
password: ref(''),
displayName: ref(''),
};
const formHandler = async (ev: Event) => {
ev.preventDefault();
const payload = {
username: data.username.value,
password: data.password.value,
displayName: data.displayName.value,
};
// loading
state.isLoading.value = true;
const user = new structs.User();
Object.assign(user, payload);
try {
await actionCb(
new structs.CreateUserRequest({
user: user,
}),
);
} finally {
state.isLoading.value = false;
}
};
const passwordToggle = () => {
state.passwordShow.value = !state.passwordShow.value;
};
return showDialog(
'create-user',
{
state,
data,
formHandler,
passwordToggle,
},
{
withClose: true,
actions: [],
},
);
}
</script>
<template>
<div class="px-2 sm:px-4 py-2.5">
<div class="container mx-auto">
<HeadingPage>List Users</HeadingPage>
<AlertBox />
<!-- control -->
<div class="mt-2">
<a
href="#"
class="text-white bg-green-700 hover:bg-green-800 focus:ring-4 focus:ring-green-300 font-medium rounded-lg text-sm px-2.5 py-2 mr-2 mb-2 dark:bg-green-600 dark:hover:bg-green-700 focus:outline-none dark:focus:ring-green-800"
@click="createUser()"
>Create User</a
>
<a
href="#"
:disabled="!hasSelectedItems"
:class="[
{ 'cursor-not-allowed': !hasSelectedItems },
'text-white bg-cyan-700 hover:bg-cyan-800 focus:ring-4 focus:ring-cyan-300 font-medium rounded-lg text-sm px-2.5 py-2 mr-2 mb-2 dark:bg-cyan-600 dark:hover:bg-cyan-700 focus:outline-none dark:focus:ring-cyan-800',
]"
@click="manageRolesSelected()"
>Manage&nbsp;Roles{{ captionBulk }}</a
>
<a
href="#"
:disabled="!hasSelectedItems"
:class="[
{ 'cursor-not-allowed': !hasSelectedItems },
'text-white bg-red-700 hover:bg-red-800 focus:ring-4 focus:ring-red-300 font-medium rounded-lg text-sm px-2.5 py-2 mr-2 mb-2 dark:bg-red-600 dark:hover:bg-red-700 focus:outline-none dark:focus:ring-red-800',
]"
@click="deleteUserSelected"
>Delete{{ captionBulk }}</a
>
<input
type="text"
placeholder="Search user/name"
v-model="searchTerm"
:class="[
'peer',
'px-2.5 py-2',
//
'w-full block mt-4',
'md:w-auto md:inline-block md:mt-0', // mobile
// 'px-1 pb-1 pt-1 text-sm',
'bg-transparent rounded-lg border-1',
'text-sm text-gray-900',
'border-gray-300 appearance-none',
'dark:text-white dark:border-gray-600',
'focus:outline-none focus:ring-0 focus:border-blue-600',
'dark:focus:border-blue-500',
]"
/>
</div>
<!-- table -->
<div class="mt-5">
<EasyDataTable
buttons-pagination
v-model:server-options="serverOptions"
v-model:items-selected="itemsSelected"
:server-items-length="serverItemsLength"
:loading="isLoading"
:headers="headers"
:items="items"
:sort-by="sortBy"
:sort-type="sortType"
:search-field="searchField"
:search-value="searchTerm"
>
<template #item-roles="{ roles }">
<span v-if="roles.length > 0">
<a
:href="'/admin/role/' + role.id"
class="text-white bg-blue-700 hover:bg-blue-800 focus:ring-4 focus:ring-blue-300 font-medium rounded-lg text-xs px-1 py-1 mr-2 mb-2 dark:bg-blue-600 dark:hover:bg-blue-700 focus:outline-none dark:focus:ring-blue-800"
v-for="(role, i) in roles"
:key="i"
>
{{ role.displayName }}
</a>
</span>
<span v-else><i>No roles</i></span>
</template>
<template #item-actions="user">
<a
href="#"
class="text-white bg-cyan-700 hover:bg-cyan-800 focus:ring-4 focus:ring-cyan-300 font-medium rounded-lg text-xs px-2 py-1.5 mr-2 mb-2 dark:bg-cyan-600 dark:hover:bg-cyan-700 focus:outline-none dark:focus:ring-cyan-800"
@click="manageRoles(user)"
>Manage&nbsp;Roles</a
>
<a
href="#"
class="text-white bg-red-700 hover:bg-red-800 focus:ring-4 focus:ring-red-300 font-medium rounded-lg text-xs px-2 py-1.5 mr-2 mb-2 dark:bg-red-600 dark:hover:bg-red-700 focus:outline-none dark:focus:ring-red-800"
@click="deleteUser(user)"
>Delete</a
>
</template>
</EasyDataTable>
</div>
</div>
</div>
<!-- modal -->
<ModalDialog
@on-show="() => (isModalShow = true)"
@on-hide="() => (isModalShow = false)"
:show="isModalShow"
:slot-view-name="modalView"
:with-close-button="modalWithClose"
>
<template #view-confirm-delete>
<div class="p-6 text-center">
<h3 class="mb-5 text-lg font-normal">
Are you sure want to delete this user{{ modalContext.length > 1 ? 's' : '' }}? <br />{{
modalContext.length <= 3
? modalContext.map((x: structs.User) => x.username).join(', ')
: `${modalContext.length} users`
}}
</h3>
<button
class="text-white bg-red-600 hover:bg-red-800 focus:ring-4 focus:outline-none focus:ring-red-300 dark:focus:ring-red-800 font-medium rounded-lg text-sm inline-flex items-center px-5 py-2.5 text-center mr-2"
@click="modalActionCb[0]"
>
Delete
</button>
<button
class="text-gray-500 bg-white hover:bg-gray-100 focus:ring-4 focus:outline-none focus:ring-gray-200 rounded-lg border border-gray-200 text-sm font-medium px-5 py-2.5 hover:text-gray-900 focus:z-10 dark:bg-gray-700 dark:text-gray-300 dark:border-gray-500 dark:hover:text-white dark:hover:bg-gray-600 dark:focus:ring-gray-600"
@click="modalActionCb[1]"
>
Cancel
</button>
</div>
</template>
<template #view-create-user>
<div class="py-6 px-6 lg:px-8">
<!-- heading -->
<h3 class="mb-4 text-xl font-medium text-gray-900 dark:text-white">Create user</h3>
<!-- body -->
<form class="space-y-6" @submit="modalContext.formHandler">
<!-- username -->
<div class="relative">
<input
v-model="modalContext.data.displayName"
type="text"
id="txt_display_name"
:disabled="isLoading"
:class="[
{ disabled: modalContext.state.isLoading },
'block px-2.5 pb-2.5 pt-4 w-full text-sm text-gray-900 bg-transparent rounded-lg border-1 border-gray-300 appearance-none dark:text-white dark:border-gray-600 dark:focus:border-blue-500 focus:outline-none focus:ring-0 focus:border-blue-600 peer',
]"
placeholder=" "
/>
<label
for="txt_display_name"
class="absolute text-sm text-gray-500 dark:text-gray-400 duration-300 transform -translate-y-4 scale-75 top-2 z-10 origin-[0] bg-white dark:bg-gray-900 px-2 peer-focus:px-2 peer-focus:text-blue-600 peer-focus:dark:text-blue-500 peer-placeholder-shown:scale-100 peer-placeholder-shown:-translate-y-1/2 peer-placeholder-shown:top-1/2 peer-focus:top-2 peer-focus:scale-75 peer-focus:-translate-y-4 left-1"
>Display Name</label
>
</div>
<!-- username -->
<div class="relative">
<input
v-model="modalContext.data.username"
type="text"
id="txt_username"
:disabled="modalContext.state.isLoading"
:class="[
{ disabled: isLoading },
'block px-2.5 pb-2.5 pt-4 w-full text-sm text-gray-900 bg-transparent rounded-lg border-1 border-gray-300 appearance-none dark:text-white dark:border-gray-600 dark:focus:border-blue-500 focus:outline-none focus:ring-0 focus:border-blue-600 peer',
]"
placeholder=" "
/>
<label
for="txt_username"
class="absolute text-sm text-gray-500 dark:text-gray-400 duration-300 transform -translate-y-4 scale-75 top-2 z-10 origin-[0] bg-white dark:bg-gray-900 px-2 peer-focus:px-2 peer-focus:text-blue-600 peer-focus:dark:text-blue-500 peer-placeholder-shown:scale-100 peer-placeholder-shown:-translate-y-1/2 peer-placeholder-shown:top-1/2 peer-focus:top-2 peer-focus:scale-75 peer-focus:-translate-y-4 left-1"
>Username</label
>
</div>
<!-- password -->
<div class="flex relative">
<input
v-model="modalContext.data.password"
:type="modalContext.state.passwordShow ? 'text' : 'password'"
id="txt_password"
:disabled="isLoading"
:class="[
{ disabled: modalContext.state.isLoading },
'block px-2.5 pb-2.5 pt-4 w-full text-sm text-gray-900 bg-transparent rounded-lg border-1 border-gray-300 appearance-none dark:text-white dark:border-gray-600 dark:focus:border-blue-500 focus:outline-none focus:ring-0 focus:border-blue-600 peer',
]"
placeholder=" "
/>
<label
for="txt_password"
class="absolute text-sm text-gray-500 dark:text-gray-400 duration-300 transform -translate-y-4 scale-75 top-2 z-10 origin-[0] bg-white dark:bg-gray-900 px-2 peer-focus:px-2 peer-focus:text-blue-600 peer-focus:dark:text-blue-500 peer-placeholder-shown:scale-100 peer-placeholder-shown:-translate-y-1/2 peer-placeholder-shown:top-1/2 peer-focus:top-2 peer-focus:scale-75 peer-focus:-translate-y-4 left-1"
>Password</label
>
<div class="absolute right-1.5 bottom-1.5">
<div href="#" @click="modalContext.passwordToggle" class="p-2">
<EyeIcon
v-if="!modalContext.state.passwordShow"
class="w-5 h-5 text-sm text-gray-900"
/>
<EyeSlashIcon v-else class="w-5 h-5 text-sm text-gray-900" />
</div>
</div>
</div>
<button
type="submit"
class="w-full text-white bg-blue-700 hover:bg-blue-800 focus:ring-4 focus:outline-none focus:ring-blue-300 font-medium rounded-lg text-sm px-5 py-2.5 text-center dark:bg-blue-600 dark:hover:bg-blue-700 dark:focus:ring-blue-800"
>
<span v-if="!modalContext.state.isLoading">Create</span>
<span v-else>
<SpinnerIconVue class="text-white" />
</span>
</button>
</form>
</div>
</template>
<template #view-update-user>
<h1>Update user</h1>
</template>
<template #view-manager-users-roles>
<h1>Manage users roles</h1>
</template>
</ModalDialog>
</template>

View file

@ -0,0 +1,16 @@
<script lang="ts" setup>
import { useAuth } from '@/stores/auth';
import { MagnifyingGlassIcon } from '@heroicons/vue/24/outline';
import HeadingPage from './components/HeadingPage.vue';
const auth = useAuth();
</script>
<template>
<div class="px-2 sm:px-4 py-2.5">
<div class="container mx-auto">
<HeadingPage
>Welcome<span v-if="auth.profile">, {{ auth.profile?.displayName }}</span></HeadingPage
>
</div>
</div>
</template>

View file

@ -0,0 +1,80 @@
<script lang="ts" setup>
import LoginCard from '@/views/core/components/LoginCard.vue';
import NavigationBar from '@/components/NavigationBar.vue';
import { useAuth } from '@/stores/auth';
import { useRoute, useRouter } from 'vue-router';
import /* * as */structs from '@thriftgen/structs_types';
import /* * as */exceptions from '@thriftgen/exceptions_types';
import { useAlert } from '@/stores/alert';
import { computed, watch } from 'vue';
const auth = useAuth();
const alert = useAlert();
const router = useRouter();
const route = useRoute();
const nextRedirect = route.redirectedFrom;
const isLoggedIn = computed(() => auth.isLoggedIn);
if (isLoggedIn.value) {
onAuthStateChange(isLoggedIn.value);
}
watch(isLoggedIn, (value) => {
onAuthStateChange(value);
});
function onAuthStateChange(value: boolean) {
console.log('auth state changed', value);
if (value) {
if (nextRedirect) {
router.push(nextRedirect.fullPath);
} else {
router.push('/');
}
console.log(window.history.length);
// if (window.history.length > 2) {
// router.back();
// } else {
// }
}
}
function onInputChange() {
alert.hide();
}
async function onLoginSuccess(resp: structs.LoginResponse) {
if (resp.trkToken) {
auth.setTrackToken(resp.trkToken);
}
if (resp.token) {
auth.setAuthToken(resp.token);
}
if (resp.profile) {
auth.setProfile(resp.profile);
}
// console.log('success check', auth.trkToken, auth.authToken);
router.push('/');
}
function onLoginFailed(e: any) {
if (e instanceof exceptions.CoreServicesException) {
const alertVal = e.parameters['alert'];
if (alertVal) {
const alertData: structs.AlertInfo = JSON.parse(alertVal);
alert.showContent(alertData.title, alertData.description);
return;
}
}
alert.showContent('Server Error', e.toString());
}
</script>
<template>
<div class="mt-11 flex items-center justify-center">
<LoginCard
@on-login-success="onLoginSuccess"
@on-login-failed="onLoginFailed"
@on-input-change="onInputChange"
/>
</div>
</template>

View file

@ -0,0 +1,358 @@
<script lang="ts" setup>
import AlertBox from '@/components/AlertBox.vue';
import { useHub } from '@/hub';
import /* * as */structs from '@thriftgen/structs_types';
import /* * as */exceptions from '@thriftgen/exceptions_types';
import { computed, ref, watch } from 'vue';
import { Header, Item, ServerOptions, SortType } from 'vue3-easy-data-table';
import HeadingPage from './components/HeadingPage.vue';
import { useAlert } from '@/stores/alert';
import ModalDialog from './components/ModalDialog.vue';
import { join } from 'path';
const hub = useHub();
const alert = useAlert();
const isLoading = ref(false);
const headers: Header[] = [
{ text: 'NPWP', value: 'profile.npwp' },
{ text: 'Name', value: 'profile.displayName' },
{ text: 'Address', value: 'profile.address' },
{ text: 'Actions', value: 'actions' },
];
const items = ref<Item[]>([]);
const sortBy: string[] = ['name'];
const sortType: SortType[] = ['desc', 'asc'];
const serverItemsLength = ref(0);
const serverOptions = ref<ServerOptions>({
page: 1,
rowsPerPage: 25,
});
const itemsSelected = ref<Item[]>([]);
const hasSelectedItems = computed(() => itemsSelected.value.length > 0);
const searchField = ['Name'];
const searchTerm = ref('');
const ownershipStatus = ref(structs.WajibPajakOwnership.OWNED);
async function fetchWajibPajakToTable() {
isLoading.value = true;
try {
console.log('svr opt:', serverOptions);
const { page, rowsPerPage } = serverOptions.value;
const pagination = new structs.Pagination({ page, rowsPerPage });
const req = new structs.GetWajibPajakListRequest({
pagination: pagination,
searchTerm: searchTerm.value,
ownership: ownershipStatus.value,
});
const resp = await hub.BizCore.client.getWajibPajakList(req);
console.log('get wps', resp);
items.value = resp.wajibPajakList;
serverItemsLength.value = resp.totalWajibPajak + 0;
} catch (e: any) {
if (e instanceof exceptions.CoreServicesException) {
const alertVal = e.parameters['alert'];
if (alertVal) {
const alertData: structs.AlertInfo = JSON.parse(alertVal);
alert.showContent(alertData.title, alertData.description);
return;
}
}
alert.showContent('Server Error', e.toString());
} finally {
isLoading.value = false;
}
}
fetchWajibPajakToTable();
watch(serverOptions, () => {
fetchWajibPajakToTable();
});
watch(searchTerm, (value) => {
fetchWajibPajakToTable();
});
watch(ownershipStatus, (value) => {
fetchWajibPajakToTable();
});
/* Event callback */
async function createWp() {
console.log('create wp');
showCreateWpDialog(async (req) => {
try {
const resp = await hub.BizCore.client.createWajibPajak(req);
console.log('create wp resp', resp);
} catch (e: any) {
if (e instanceof exceptions.CoreServicesException) {
const alertVal = e.parameters['alert'];
if (alertVal) {
const alertData: structs.AlertInfo = JSON.parse(alertVal);
alert.showContent(alertData.title, alertData.description);
return;
}
}
alert.showContent('Server Error', e.toString());
} finally {
hideDialog();
fetchWajibPajakToTable();
}
});
}
/* Modal script */
const isModalShow = ref(false);
const modalView = ref('');
const modalWithClose = ref(false);
const modalContext = ref({} as any);
const modalActionCb = ref<(() => void)[]>([]);
function showDialog(
view: string,
ctx: any,
opt?: {
withClose?: boolean;
actions?: ((resolve: (value: boolean | PromiseLike<boolean>) => void) => void)[];
},
): Promise<boolean> {
isModalShow.value = true;
modalWithClose.value = (opt || {}).withClose || false;
modalView.value = view;
modalContext.value = ctx;
return new Promise((resolve) => {
const actions = ((opt || {}).actions || []).map((act) => () => {
isModalShow.value = false; // TODO: make this optional
act(resolve);
});
modalActionCb.value = actions;
});
}
function hideDialog() {
isModalShow.value = false;
}
function showCreateWpDialog(
actionCb: (req: structs.CreateWajibPajakRequest) => Promise<void>,
): Promise<boolean> {
const state = { isLoading: ref(false) };
const data = {
npwp: ref(''),
displayName: ref(''),
address: ref(''),
};
const formHandler = async (ev: Event) => {
ev.preventDefault();
const payload = {
npwp: data.npwp.value,
displayName: data.displayName.value,
address: data.address.value,
};
state.isLoading.value = true;
const wp = new structs.WajibPajak();
wp.profile = new structs.WajibPajakProfile(payload);
try {
await actionCb(
new structs.CreateWajibPajakRequest({
wajibPajak: wp,
}),
);
} finally {
state.isLoading.value = false;
}
};
return showDialog(
'create-wp',
{
state,
data,
formHandler,
},
{ withClose: true },
);
}
</script>
<template>
<div class="px-2 sm:px-4 py-2.5">
<div class="container mx-auto">
<HeadingPage>List Wajib Pajak</HeadingPage>
<AlertBox />
<!-- control -->
<div class="mt-2">
<a
href="#"
@click="createWp"
class="text-white bg-green-700 hover:bg-green-800 focus:ring-4 focus:ring-green-300 font-medium rounded-lg text-sm px-2.5 py-2 mr-2 mb-2 dark:bg-green-600 dark:hover:bg-green-700 focus:outline-none dark:focus:ring-green-800"
>Create Wajib Pajak</a
>
<input
type="text"
placeholder="Search name"
v-model="searchTerm"
:class="[
'peer',
'px-2.5 py-2',
//
'w-full block mt-4',
'md:w-auto md:inline-block md:mt-0', // mobile
// 'px-1 pb-1 pt-1 text-sm',
'bg-transparent rounded-lg border-1',
'text-sm text-gray-900',
'border-gray-300 appearance-none',
'dark:text-white dark:border-gray-600',
'focus:outline-none focus:ring-0 focus:border-blue-600',
'dark:focus:border-blue-500',
]"
/>
</div>
<!-- body -->
<div class="mt-5">
<ul
class="flex flex-wrap text-sm font-medium text-center text-gray-500 border-b border-gray-200 dark:border-gray-700 dark:text-gray-400"
>
<li
v-for="(tab, i) in [
{
caption: 'Owned',
value: structs.WajibPajakOwnership.OWNED,
},
{
caption: 'Role Access',
value: structs.WajibPajakOwnership.ROLE,
},
]"
:aria-current="tab.value == ownershipStatus ? 'page' : 'false'"
:key="i"
class="mr-2"
>
<a
href="#"
@click="ownershipStatus = tab.value"
:aria-current="tab.value == ownershipStatus ? 'page' : 'false'"
:class="[
'inline-block p-4 rounded-t-lg',
'dark:hover:bg-gray-800',
tab.value == ownershipStatus
? 'active text-blue-600 bg-gray-100 dark:text-blue-500' //
: 'hover:test-gray-600 hover:bg-gray-50 dark:hover:text-gray-300', //
]"
>{{ tab.caption }}</a
>
</li>
</ul>
<EasyDataTable
buttons-pagination
v-model:server-options="serverOptions"
v-model:items-selected="itemsSelected"
:server-items-length="serverItemsLength"
:loading="isLoading"
:headers="headers"
:items="items"
:sort-by="sortBy"
:sort-type="sortType"
:search-field="searchField"
:search-value="searchTerm"
>
</EasyDataTable>
</div>
</div>
</div>
<!-- modal -->
<ModalDialog
@on-show="() => (isModalShow = true)"
@on-hide="() => (isModalShow = false)"
:show="isModalShow"
:slot-view-name="modalView"
:with-close-button="modalWithClose"
>
<template #view-create-wp>
<div class="py-6 px-6 lg:px-8">
<!-- heading -->
<h3 class="mb-4 text-xl font-medium text-gray-900 dark:text-white">Create Wajib Pajak</h3>
<!-- body -->
<form class="space-y-6" @submit="modalContext.formHandler">
<!-- npwp -->
<div class="relative">
<input
v-model="modalContext.data.npwp"
type="text"
id="txt_npwp"
:disabled="isLoading"
:class="[
{ disabled: modalContext.state.isLoading },
'block px-2.5 pb-2.5 pt-4 w-full text-sm text-gray-900 bg-transparent rounded-lg border-1 border-gray-300 appearance-none dark:text-white dark:border-gray-600 dark:focus:border-blue-500 focus:outline-none focus:ring-0 focus:border-blue-600 peer',
]"
placeholder=" "
/>
<label
for="txt_npwp"
class="absolute text-sm text-gray-500 dark:text-gray-400 duration-300 transform -translate-y-4 scale-75 top-2 z-10 origin-[0] bg-white dark:bg-gray-900 px-2 peer-focus:px-2 peer-focus:text-blue-600 peer-focus:dark:text-blue-500 peer-placeholder-shown:scale-100 peer-placeholder-shown:-translate-y-1/2 peer-placeholder-shown:top-1/2 peer-focus:top-2 peer-focus:scale-75 peer-focus:-translate-y-4 left-1"
>NPWP</label
>
</div>
<!-- displayName -->
<div class="relative">
<input
v-model="modalContext.data.displayName"
type="text"
id="txt_display_name"
:disabled="isLoading"
:class="[
{ disabled: modalContext.state.isLoading },
'block px-2.5 pb-2.5 pt-4 w-full text-sm text-gray-900 bg-transparent rounded-lg border-1 border-gray-300 appearance-none dark:text-white dark:border-gray-600 dark:focus:border-blue-500 focus:outline-none focus:ring-0 focus:border-blue-600 peer',
]"
placeholder=" "
/>
<label
for="txt_display_name"
class="absolute text-sm text-gray-500 dark:text-gray-400 duration-300 transform -translate-y-4 scale-75 top-2 z-10 origin-[0] bg-white dark:bg-gray-900 px-2 peer-focus:px-2 peer-focus:text-blue-600 peer-focus:dark:text-blue-500 peer-placeholder-shown:scale-100 peer-placeholder-shown:-translate-y-1/2 peer-placeholder-shown:top-1/2 peer-focus:top-2 peer-focus:scale-75 peer-focus:-translate-y-4 left-1"
>Display Name</label
>
</div>
<!-- address -->
<div class="relative">
<input
v-model="modalContext.data.address"
type="text"
id="txt_address"
:disabled="isLoading"
:class="[
{ disabled: modalContext.state.isLoading },
'block px-2.5 pb-2.5 pt-4 w-full text-sm text-gray-900 bg-transparent rounded-lg border-1 border-gray-300 appearance-none dark:text-white dark:border-gray-600 dark:focus:border-blue-500 focus:outline-none focus:ring-0 focus:border-blue-600 peer',
]"
placeholder=" "
/>
<label
for="txt_address"
class="absolute text-sm text-gray-500 dark:text-gray-400 duration-300 transform -translate-y-4 scale-75 top-2 z-10 origin-[0] bg-white dark:bg-gray-900 px-2 peer-focus:px-2 peer-focus:text-blue-600 peer-focus:dark:text-blue-500 peer-placeholder-shown:scale-100 peer-placeholder-shown:-translate-y-1/2 peer-placeholder-shown:top-1/2 peer-focus:top-2 peer-focus:scale-75 peer-focus:-translate-y-4 left-1"
>Address</label
>
</div>
<button
type="submit"
class="w-full text-white bg-blue-700 hover:bg-blue-800 focus:ring-4 focus:outline-none focus:ring-blue-300 font-medium rounded-lg text-sm px-5 py-2.5 text-center dark:bg-blue-600 dark:hover:bg-blue-700 dark:focus:ring-blue-800"
>
<span v-if="!modalContext.state.isLoading">Create</span>
<span v-else>
<SpinnerIconVue class="text-white" />
</span>
</button>
</form>
</div>
</template>
</ModalDialog>
</template>

View file

@ -0,0 +1,5 @@
<template>
<h2 class="font-medium leading-tight text-3xl mt-0 mb-2 text-blue-600">
<slot></slot>
</h2>
</template>

View file

@ -0,0 +1,131 @@
<script lang="ts" setup>
import { useHub } from '@/hub';
import /* * as */structs from '@thriftgen/structs_types';
import { EyeIcon, EyeSlashIcon } from '@heroicons/vue/24/solid';
import { computed, ref, watch } from 'vue';
import AlertBox from '@/components/AlertBox.vue';
import SpinnerIcon from '@/components/SpinnerIcon.vue';
const hub = useHub();
const emit = defineEmits(['togglePassword', 'onInputChange', 'onLoginSuccess', 'onLoginFailed']);
const isPasswordShow = ref(false);
const isLoading = ref(false);
const txtUsername = ref('');
const txtPassword = ref('');
const formData = computed(() => ({ username: txtUsername.value, password: txtPassword.value }));
// togglePassword to show/hide the password
function togglePassword(ev: Event) {
ev.preventDefault();
isPasswordShow.value = !isPasswordShow.value;
emit('togglePassword', isPasswordShow);
}
// emit event if there's change in username and/or password
watch(txtUsername, () => {
emit('onInputChange');
});
watch(txtPassword, () => {
emit('onInputChange');
});
// form submit handler
async function formSubmit(ev: Event) {
ev.preventDefault();
isLoading.value = true; // set this to true first
try {
const req = new structs.LoginRequest({
...formData.value,
});
const res = await hub.BizCore.client.login(req);
if (res) {
emit('onLoginSuccess', res);
return;
}
console.error('unreachable');
} catch (e: any) {
emit('onLoginFailed', e);
} finally {
isLoading.value = false;
}
}
</script>
<template>
<div
class="block p-6 max-w-lg w-2/3 bg-white rounded-lg border border-gray-200 shadow-md hover:bg-gray-100 dark:bg-gray-800 dark:border-gray-700 dark:hover:bg-gray-700"
>
<h5 class="mb-2 text-2xl font-bold tracking-tight text-gray-900 dark:text-white">Login</h5>
<form class="mt-6" @submit="formSubmit">
<AlertBox />
<div class="space-y-4">
<!-- username -->
<div class="relative">
<input
v-model="txtUsername"
type="text"
id="txt_username"
:disabled="isLoading"
:class="[
{ disabled: isLoading },
'block px-2.5 pb-2.5 pt-4 w-full text-sm text-gray-900 bg-transparent rounded-lg border-1 border-gray-300 appearance-none dark:text-white dark:border-gray-600 dark:focus:border-blue-500 focus:outline-none focus:ring-0 focus:border-blue-600 peer',
]"
placeholder=" "
/>
<label
for="txt_username"
class="absolute text-sm text-gray-500 dark:text-gray-400 duration-300 transform -translate-y-4 scale-75 top-2 z-10 origin-[0] bg-white dark:bg-gray-900 px-2 peer-focus:px-2 peer-focus:text-blue-600 peer-focus:dark:text-blue-500 peer-placeholder-shown:scale-100 peer-placeholder-shown:-translate-y-1/2 peer-placeholder-shown:top-1/2 peer-focus:top-2 peer-focus:scale-75 peer-focus:-translate-y-4 left-1"
>Username</label
>
</div>
<!-- password -->
<div class="flex relative">
<input
v-model="txtPassword"
:type="isPasswordShow ? 'text' : 'password'"
id="txt_password"
:disabled="isLoading"
:class="[
{ disabled: isLoading },
'block px-2.5 pb-2.5 pt-4 w-full text-sm text-gray-900 bg-transparent rounded-lg border-1 border-gray-300 appearance-none dark:text-white dark:border-gray-600 dark:focus:border-blue-500 focus:outline-none focus:ring-0 focus:border-blue-600 peer',
]"
placeholder=" "
/>
<label
for="txt_password"
class="absolute text-sm text-gray-500 dark:text-gray-400 duration-300 transform -translate-y-4 scale-75 top-2 z-10 origin-[0] bg-white dark:bg-gray-900 px-2 peer-focus:px-2 peer-focus:text-blue-600 peer-focus:dark:text-blue-500 peer-placeholder-shown:scale-100 peer-placeholder-shown:-translate-y-1/2 peer-placeholder-shown:top-1/2 peer-focus:top-2 peer-focus:scale-75 peer-focus:-translate-y-4 left-1"
>Password</label
>
<div class="absolute right-1.5 bottom-1.5">
<div href="#" @click="togglePassword" class="p-2">
<EyeIcon v-if="!isPasswordShow" class="w-5 h-5 text-sm text-gray-900" />
<EyeSlashIcon v-else class="w-5 h-5 text-sm text-gray-900" />
</div>
</div>
</div>
</div>
<div class="flex items-baseline justify-between mt-7">
<button
type="submit"
:disabled="isLoading"
:class="[
{ disabled: isLoading },
'text-white w-full bg-blue-700 hover:bg-blue-800 focus:ring-4 focus:ring-blue-300 font-medium rounded-lg text-sm px-5 py-2.5 mb-2 dark:bg-blue-600 dark:hover:bg-blue-700 focus:outline-none dark:focus:ring-blue-800',
]"
>
<span v-if="!isLoading">Login</span>
<span v-else>
<SpinnerIcon class="text-white" />
Loading...
</span>
</button>
<!-- <a href="#" class="text-sm text-blue-600 hover:underline">Forgot password?</a> -->
</div>
</form>
</div>
</template>

View file

@ -0,0 +1,139 @@
<!-- eslint-disable no-undef -->
<!-- eslint-disable prefer-const -->
<script lang="ts" setup>
import { computed, onMounted, ref, useSlots, watch } from 'vue';
const props = defineProps<{
show?: boolean;
withCloseButton?: boolean;
slotViewName?: string;
placement?: string;
backdropClasses?: string;
}>();
const emit = defineEmits(['onHide', 'onShow', 'onToggle']);
const slots = useSlots();
const refTargetEl = ref();
let modal: any;
function toggle() {
modal.toggle();
}
function show() {
modal.show();
}
function hide() {
modal.hide();
}
onMounted(() => {
const targetEl = refTargetEl.value;
let options: any = {
onHide: () => {
console.log('modal hide');
emit('onHide');
},
onShow: () => {
console.log('modal show');
emit('onShow');
},
onToggle: () => {
console.log('modal toggle');
emit('onToggle');
},
};
if (props.backdropClasses) options.backdropClasses = props.backdropClasses;
if (props.placement) options.placement = props.placement;
// @ts-ignore
modal = new Modal(targetEl, options);
watch(
() => props.show,
(value) => {
console.log('show changed', props.show);
if (value) {
show();
} else {
hide();
}
},
);
});
const hasDefaultSlot = computed(() => slots.default);
</script>
<template>
<!-- modal -->
<div
ref="refTargetEl"
tabindex="-1"
class="hidden overflow-y-auto overflow-x-hidden fixed top-0 right-0 left-0 z-50 md:inset-0 h-modal md:h-full"
>
<div class="relative p-4 w-full max-w-md h-full md:h-auto">
<div class="relative bg-white rounded-lg shadow dark:bg-gray-700">
<button
v-if="props.withCloseButton"
type="button"
class="absolute top-3 right-2.5 text-gray-400 bg-transparent hover:bg-gray-200 hover:text-gray-900 rounded-lg text-sm p-1.5 ml-auto inline-flex items-center dark:hover:bg-gray-800 dark:hover:text-white"
data-modal-toggle="popup-modal"
@click="hide()"
>
<svg
aria-hidden="true"
class="w-5 h-5"
fill="currentColor"
viewBox="0 0 20 20"
xmlns="http://www.w3.org/2000/svg"
>
<path
fill-rule="evenodd"
d="M4.293 4.293a1 1 0 011.414 0L10 8.586l4.293-4.293a1 1 0 111.414 1.414L11.414 10l4.293 4.293a1 1 0 01-1.414 1.414L10 11.414l-4.293 4.293a1 1 0 01-1.414-1.414L8.586 10 4.293 5.707a1 1 0 010-1.414z"
clip-rule="evenodd"
></path>
</svg>
<span class="sr-only">Close modal</span>
</button>
<!-- <div class="p-6 text-center">
<svg
aria-hidden="true"
class="mx-auto mb-4 w-14 h-14 text-gray-400 dark:text-gray-200"
fill="none"
stroke="currentColor"
viewBox="0 0 24 24"
xmlns="http://www.w3.org/2000/svg"
>
<path
stroke-linecap="round"
stroke-linejoin="round"
stroke-width="2"
d="M12 8v4m0 4h.01M21 12a9 9 0 11-18 0 9 9 0 0118 0z"
></path>
</svg>
<h3 class="mb-5 text-lg font-normal text-gray-500 dark:text-gray-400">
Are you sure you want to delete this product?
</h3>
<button
data-modal-toggle="popup-modal"
type="button"
class="text-white bg-red-600 hover:bg-red-800 focus:ring-4 focus:outline-none focus:ring-red-300 dark:focus:ring-red-800 font-medium rounded-lg text-sm inline-flex items-center px-5 py-2.5 text-center mr-2"
>
Yes, I'm sure
</button>
<button
data-modal-toggle="popup-modal"
type="button"
class="text-gray-500 bg-white hover:bg-gray-100 focus:ring-4 focus:outline-none focus:ring-gray-200 rounded-lg border border-gray-200 text-sm font-medium px-5 py-2.5 hover:text-gray-900 focus:z-10 dark:bg-gray-700 dark:text-gray-300 dark:border-gray-500 dark:hover:text-white dark:hover:bg-gray-600 dark:focus:ring-gray-600"
>
No, cancel
</button>
</div> -->
<slot :name="`view-${props.slotViewName}`" />
</div>
</div>
</div>
</template>

View file

@ -0,0 +1,63 @@
import { RouteRecordRaw } from 'vue-router';
import AdminUserCreateViewVue from './AdminUserCreateView.vue';
import AdminUserListViewVue from './AdminUserListView.vue';
import HomeView from './HomeView.vue';
import LoginView from './LoginView.vue';
import UserWajibPajakListViewVue from './UserWajibPajakListView.vue';
export function routes(): RouteRecordRaw[] {
return [
{
path: '/',
name: 'core_home',
component: HomeView,
meta: {
requiresAuth: false,
navbarLink: {
caption: 'Home',
priority: 5,
},
},
},
{
path: '/login',
name: 'core_login',
component: LoginView,
meta: {
requresAuth: false,
},
},
// wajib pajak
{
path: '/wajib-pajak/list',
name: 'core_wajibpajak_list',
component: UserWajibPajakListViewVue,
meta: {
requiresAuth: true,
navbarLink: {
caption: 'Wajib Pajak',
},
},
},
// admin
{
path: '/admin/users',
name: 'core_admin_user_list',
component: AdminUserListViewVue,
meta: {
requiresAuth: true,
navbarLink: {
caption: 'Users',
},
},
},
];
}
export default {
routes,
};

View file

@ -0,0 +1,32 @@
<script setup lang="ts">
import { useRouter } from 'vue-router';
const router = useRouter();
function goBack() {
router.back();
}
</script>
<template>
<div class="">
<div class="grid place-items-center">
<div class="inline-flex">
<EmojiSadIcon class="w-10 h-10 text-gray-900 mr-3" />
<div class="justify-center items-center text-center">
<h1 class="text-gray-900 text-3xl leading-tight font-medium mb-2">404 Not Found</h1>
</div>
</div>
Page or resource is not available, that's all we know.
<div class="mt-4">
<button
type="button"
@click="goBack"
class="text-white bg-blue-700 hover:bg-blue-800 focus:ring-4 focus:ring-blue-300 font-medium rounded-lg text-sm px-5 py-2.5 mr-2 mb-2 dark:bg-blue-600 dark:hover:bg-blue-700 focus:outline-none dark:focus:ring-blue-800"
>
Go Back
</button>
</div>
</div>
</div>
</template>

View file

@ -0,0 +1,16 @@
import { type RouteRecordRaw } from 'vue-router';
import NotFoundView from './NotFoundView.vue';
export function routes(): RouteRecordRaw[] {
return [
{
path: '/:pathMatch(.*)*',
name: 'error_not_found',
component: NotFoundView,
},
];
}
export default {
routes,
};

8
frontend/src/vite-env.d.ts vendored Normal file
View file

@ -0,0 +1,8 @@
/// <reference types="vite/client" />
declare module '*.vue' {
import type { DefineComponent } from 'vue';
const component: DefineComponent<{}, {}, any>;
export default component;
}

View file

@ -0,0 +1,32 @@
// import colors from 'tailwindcss/colors';
// import fbplugin from 'flowbite/plugin';
/** @type {import('tailwindcss').Config} */
module.exports = {
darkMode: 'class',
content: [
'./index.html',
'./src/**/*.{vue,js,ts,jsx,tsx}',
'./node_modules/flowbite/**/*.js',
'./node_modules/flowbite-vue/**/*.{js,jsx,ts,tsx}',
'./3rdparty/flowbite-vue/**/*.{vue,js,jsx,ts,tsx}',
],
theme: {
// colors: {
// gray: colors.coolGray,
// blue: colors.lightBlue,
// red: colors.rose,
// pink: colors.fuchsia,
// },
// fontFamily: {
// sans: ['Graphik', 'sans-serif'],
// serif: ['Merriweather', 'serif'],
// },
extend: {},
},
plugins: [
// require('@acmecorp/base-tailwind-config'),
require('flowbite/plugin'),
],
};

36
frontend/tsconfig.json Normal file
View file

@ -0,0 +1,36 @@
{
"compilerOptions": {
"paths": {
"@/*": ["./src/*"],
"@thriftgen/*": ["./gen/*"],
// "@flowbite-vue": ["./3rdparty/flowbite-vue/src/*"],
},
// "target": "ESNext",
// "module": "ESNext",
"module": "ESNext",
"target": "ES5",
"sourceMap": true,
"useDefineForClassFields": true,
"moduleResolution": "Node",
"strict": true,
"strictNullChecks": true,
"allowJs": true,
"jsx": "preserve",
"resolveJsonModule": true,
"isolatedModules": true,
"esModuleInterop": true,
"noImplicitReturns": true,
"lib": ["ESNext", "DOM"],
"plugins": [{ "name": "@vuedx/typescript-plugin-vue" }],
"skipLibCheck": true,
"noEmit": true
},
"include": [
"env.d.ts",
"src/**/*.ts",
"src/**/*.d.ts",
"src/**/*.tsx",
"src/**/*.vue",
],
"references": [{ "path": "./tsconfig.node.json" }]
}

View file

@ -0,0 +1,15 @@
{
"compilerOptions": {
"composite": true,
"module": "ESNext",
"moduleResolution": "Node",
"allowSyntheticDefaultImports": true,
"resolveJsonModule": true,
},
"include": [
"package.json",
"vite.config.ts",
"vite-polyfill-aliases.ts",
]
}

View file

@ -0,0 +1,32 @@
export default {
util: 'rollup-plugin-node-polyfills/polyfills/util',
sys: 'util',
events: 'rollup-plugin-node-polyfills/polyfills/events',
stream: 'rollup-plugin-node-polyfills/polyfills/stream',
path: 'rollup-plugin-node-polyfills/polyfills/path',
querystring: 'rollup-plugin-node-polyfills/polyfills/qs',
punycode: 'rollup-plugin-node-polyfills/polyfills/punycode',
url: 'rollup-plugin-node-polyfills/polyfills/url',
string_decoder: 'rollup-plugin-node-polyfills/polyfills/string-decoder',
http: 'rollup-plugin-node-polyfills/polyfills/http',
https: 'rollup-plugin-node-polyfills/polyfills/http',
os: 'rollup-plugin-node-polyfills/polyfills/os',
assert: 'rollup-plugin-node-polyfills/polyfills/assert',
constants: 'rollup-plugin-node-polyfills/polyfills/constants',
_stream_duplex: 'rollup-plugin-node-polyfills/polyfills/readable-stream/duplex',
_stream_passthrough: 'rollup-plugin-node-polyfills/polyfills/readable-stream/passthrough',
_stream_readable: 'rollup-plugin-node-polyfills/polyfills/readable-stream/readable',
_stream_writable: 'rollup-plugin-node-polyfills/polyfills/readable-stream/writable',
_stream_transform: 'rollup-plugin-node-polyfills/polyfills/readable-stream/transform',
timers: 'rollup-plugin-node-polyfills/polyfills/timers',
console: 'rollup-plugin-node-polyfills/polyfills/console',
vm: 'rollup-plugin-node-polyfills/polyfills/vm',
zlib: 'rollup-plugin-node-polyfills/polyfills/zlib',
tty: 'rollup-plugin-node-polyfills/polyfills/tty',
domain: 'rollup-plugin-node-polyfills/polyfills/domain',
buffer: 'rollup-plugin-node-polyfills/polyfills/buffer-es6',
process: 'rollup-plugin-node-polyfills/polyfills/process-es6',
// Buffer: 'buffer/',
// process: 'rollup-plugin-node-globals',
};

349
frontend/vite.config.ts Normal file
View file

@ -0,0 +1,349 @@
import { fileURLToPath, URL } from 'node:url';
import { defineConfig, optimizeDeps } from 'vite';
import vue from '@vitejs/plugin-vue';
import vueJsx from '@vitejs/plugin-vue-jsx';
import { esbuildCommonjs, viteCommonjs } from '@originjs/vite-plugin-commonjs';
import { NodeGlobalsPolyfillPlugin } from '@esbuild-plugins/node-globals-polyfill';
import { NodeModulesPolyfillPlugin } from '@esbuild-plugins/node-modules-polyfill';
import { Plugin } from 'rollup';
import { splitVendorChunkPlugin } from 'vite';
import rollupNodePolyfills from 'rollup-plugin-node-polyfills';
import viteNodePolyfills from 'vite-plugin-node-stdlib-browser';
import { nodeResolve as rollupNodeResolve } from '@rollup/plugin-node-resolve';
import rollupCommonjs from '@rollup/plugin-commonjs';
import terser from '@rollup/plugin-terser';
import compiler from '@ampproject/rollup-plugin-closure-compiler';
import { babel, getBabelOutputPlugin } from '@rollup/plugin-babel';
import legacy from '@vitejs/plugin-legacy';
import uglify from '@lopatnov/rollup-plugin-uglify';
import { obfuscator } from 'rollup-obfuscator';
import { chunkSplitPlugin } from 'vite-plugin-chunk-split';
import { dependencies } from './package.json';
import polyfillAliases from './vite-polyfill-aliases';
import { resolve } from 'node:path';
export function injectSecPlugin(mode) {
return ['production', 'live'].indexOf(mode) !== -1 ? secPlugins : [];
}
export const secPlugins: Plugin[] = [
// NOT WORK.
// babel({
// babelHelpers: 'bundled',
// presets: [[
// "@babel/preset-env",
// {
// "corejs": "3.22",
// "useBuiltIns": "entry",//"usage",
// "targets": {
// "ie": "11"
// }
// }
// ]],
// }),
// terser({
// parse: {},
// // compress: {
// // dead_code: true,
// // keep_fnames: false,
// // },
// // mangle: {
// // keep_fnames: false,
// // properties: {
// // builtins: false,
// // // keep_quoted: false,
// // // nth_identifier: ,
// // // reserved: [
// // // '_mergeNamespaces',
// // // 'polyfill',
// // // 'class', 'alt', 'src', 'href', 'target', "value", "type", "onClick"
// // // ],
// // // regex: /^(T*Protocol|T*Transport|T*Error|transport|*Exception|write*|read*|skip|*Zigzag|zigzag*|VERSION*|TYPE*|COMPACT*|HEADER*|FLAG*|TINFO*|MAX*|Varint*|varint*|CT_*|*TType|flush)$/,
// // // regex: /(thrift|Thrift|Protocol|Transport|Error|transport|Exception|write|read|skip|zigzag|VERSION|TYPE|COMPACT|HEADER|FLAG|TINFO|MAX|Varint|varint|CT|TType|flush)/,
// // },
// // },
// mangle: {
// keep_classnames: true,
// keep_fnames: true,
// properties: {
// keep_quoted: true,
// // builtins: false,
// // ||SET|LIST|||I64|
// // regex: /(^_|^is|eject|esolve|set|next|query|create|STRING|UTF7|UTF8|UTF16|DOUBLE|Error|error|dir|MAP|STOP|VOID|thrift|PROTOCOL|METHOD|method|UNKNOWN|TRANSFORM|VALID|SEQUENCE|RESULT|ERROR|CALL|REPLY|stack|field|Field|ONEWAY|EXCEPTION|client|Client|Type|socket|watch|mount|install|comp|emit|prop|destroy|Recv|Send|service|Service|Thrift|Protocol|Transport|transport|protocol|output|Error|transport|Exception|write|read|skip|zigzag|VERSION|TYPE|COMPACT|HEADER|FLAG|TINFO|MAX|Varint|varint|CT|TType|trans|call|url|xhr|XHR|header|log|before|after|update|insert|console|read|write|flush)/,
// regex: new RegExp(
// [
// // var prefixed with underscore
// '^_',
// // thrift words
// ...(
// '' +
// // 'Thrift|thrift|Field|^field|Protocol|Transport|Connection|Client|Protocol|Service|Type|Error|Exception|' +
// // // 'Recv|recv|Send|send|'+
// // 'write|read|Socket|socket|trans|prot|rotoco|tack|ervice|Trans|' +
// // 'CALL|REPLY|ONEWAY|VALID|^UNK|BAD_|IMPL|_LIMIT|SIZE_|EXCEPTION|RESULT|ERROR|TRANSFORM|PROTOCOL|VERSION|TYPE|CT|TType|METHOD|' +
// // '^STOP|^VOID|^STRING|^LIST|^MAP|^SET|^UTF7|^UTF8|^UTF16|^DOUBLE|^I64|^I32|^I16|^BOOL|^BYTE|^I08|' +
// // 'headers|igzag|^xhr|^XHR|^flush' +
// ''
// )
// .split('|')
// .map((x) => x.trim())
// .filter((x) => x != ''),
// ].join('|'),
// '',
// ),
// },
// // eval: true,
// // module: true,
// // toplevel: true,
// // safari10: true,
// // properties: false,
// },
// format: {
// comments: 'some',
// },
// }),
// compiler({
// // https://github.com/google/closure-compiler/wiki/Flags-and-Options
// // https://github.com/google/closure-compiler/blob/5707cfe4fa3be9cfe9b2f713a47f0080b08c57cb/src/com/google/javascript/jscomp/parsing/ParserRunner.java#L176
// // compilation_level: 'ADVANCED',
// // warning_level: 'VERBOSE',
// // language_in: 'ECMASCRIPT_NEXT',
// // language_in: 'STABLE',
// // current workaround, modify renderChunk preCompileOutput.
// // ConstTransform pre
// // LiteralComputedKeys post
// language_in: 'unstable',
// // language_out: 'ECMASCRIPT_NEXT',
// jscomp_off: ['undefinedVars'],
// }),
obfuscator({
compact: true,
forceTransformStrings: Array.from(
new Set([
'thrift',
'Thrift',
'vite',
'Vite',
'vue',
'Vue',
...Object.keys(dependencies)
.map((x) => x.replace('@', '-').replace('/', '-'))
.map((x) => x.split('-'))
.flat()
.filter((x) => x != ''),
]),
),
splitStrings: true,
stringArray: true,
// stringArrayEncoding: ['base64', 'none'], // 'rc4',
stringArrayThreshold: 0.8,
// stringArrayIndexShift: true,
// stringArrayWrappersChainedCalls: true,
// stringArrayRotate: true,
// stringArrayCallsTransform: true,
selfDefending: false,
// controlFlowFlattening: true,
// controlFlowFlatteningThreshold: 0.8,
// transformObjectKeys: true,
renameGlobals: false,
// BROKEN!
// renameProperties: true,
// renamePropertiesMode: 'safe',
}),
terser({
compress: {
reduce_funcs: true,
reduce_vars: true,
},
mangle: {
properties: {
// builtins: false,
keep_quoted: true,
regex: /^_/, // obfuscator underscore variable
},
eval: true,
module: true,
toplevel: true,
safari10: true,
},
format: {
comments: 'some',
},
}),
];
// https://vitejs.dev/config/
export default defineConfig(({ command, mode, ssrBuild }) => {
return {
plugins: [viteCommonjs(), vue(), vueJsx()],
resolve: {
alias: {
'@': fileURLToPath(new URL('./src', import.meta.url)),
'@thriftgen': fileURLToPath(new URL('./gen', import.meta.url)),
...polyfillAliases,
},
},
define: {
global: 'window',
},
optimizeDeps: {
esbuildOptions: {
define: { global: 'globalThis' },
plugins: [
NodeGlobalsPolyfillPlugin({
process: true,
buffer: true,
}),
NodeModulesPolyfillPlugin(),
],
},
// exclude: ['flowbite', 'flowbite-vue', 'thrift'],
},
build: {
outDir: '../public',
manifest: true,
minify: false,
rollupOptions: {
input: {
main: resolve(__dirname, 'index.html'),
},
output: {
// to be able to use manual chunk - iife
// inlineDynamicImports: true,
manualChunks(id, { getModuleInfo, getModuleIds }) {
// const dynImpIds = getModuleInfo(id).dynamicImporters;
// if (/(gen\/|thrift|Thrift|util|events|inherits|WebSocket|Int64|ws)/.test(id)) return 'ext';
// if (/flowbite/.test(id)) return 'flowbite';
// if (/data-table/.test(id)) return 'edt';
// if (/scrollbar/.test(id)) return 'scrollbar';
// if (/(node_modules)/.test(id)) return 'vendor';
// if (id in dependencies) {
// return 'vendor';
// }
},
},
plugins: [
rollupNodePolyfills(),
rollupNodeResolve(),
rollupCommonjs({}),
esbuildCommonjs(['thrift']),
],
},
},
};
});
export const x = defineConfig(({ command, mode, ssrBuild }) => {
return {
plugins: [viteNodePolyfills(), viteCommonjs(), vue(), vueJsx()],
resolve: {
alias: {
'@': fileURLToPath(new URL('./src', import.meta.url)),
'@thriftgen': fileURLToPath(new URL('./gen', import.meta.url)),
// '@flowbite-vue': fileURLToPath(new URL('./3rdparty/flowbite-vue/src', import.meta.url)),
...polyfillAliases,
},
},
optimizeDeps: {
esbuildOptions: {
define: { global: 'globalThis' },
plugins: [
NodeGlobalsPolyfillPlugin({
process: true,
buffer: true,
}),
NodeModulesPolyfillPlugin(),
],
},
// include: [],
// exclude: [/@thriftgen/],
// exclude: [
// 'flowbite',
// 'flowbite-vue',
// 'thrift',
// 'vue3-easy-data-table',
// 'vue3-perfect-scrollbar',
// ...['structs_types', 'exceptions_types', 'CoreService'].map((x) => '@thriftgen/' + x),
// ],
},
build: {
outDir: '../public',
manifest: true,
// minify: 'terser',
minify: false,
// target: 'esnext',
rollupOptions: {
// input: {
// main: resolve(__dirname, 'index.html'),
// },
// output: {
// // format: 'iife',
// // format: 'cjs',
// globals: {
// // vue: "Vue"
// },
// // to be able to use manual chunk - iife
// // inlineDynamicImports: true,
// // manualChunks(id, { getModuleInfo, getModuleIds }) {
// // // if (/(gen\/|thrift|Thrift|util|inherits|WebSocket|Int64)/.test(id)) return 'ext';
// // // const dynImpIds = getModuleInfo(id).dynamicImporters;
// // if (/data-table/.test(id)) return 'edt';
// // if (/(node_modules)/.test(id)) return 'vendor';
// // // if (id in dependencies) {
// // // return 'vendor';
// // // }
// // },
// },
external: [
// "vue",
],
plugins: [
// chunkSplitPlugin({
// strategy: 'single-vendor',
// customSplitting: {
// 'thrift-vendor': ['thrift'],
// 'flowbite-vendor': ['flowbite'],
// },
// }),
// splitVendorChunkPlugin(),
rollupNodePolyfills(),
rollupNodeResolve(),
rollupCommonjs({}),
esbuildCommonjs(['thrift']),
// NOT WORK.
// ...legacy({
// targets: ['defaults', 'IE 11'],
// }),
...injectSecPlugin(mode),
],
},
},
};
});

File diff suppressed because one or more lines are too long

45
go.mod Normal file
View file

@ -0,0 +1,45 @@
module wpw-common
go 1.19
replace github.com/cloudwego/thriftgo => github.com/ii64/thriftgo v0.2.1-2
require (
github.com/apache/thrift v0.17.0
github.com/cloudwego/thriftgo v0.0.0-00010101000000-000000000000
github.com/go-playground/locales v0.14.0
github.com/go-playground/universal-translator v0.18.0
github.com/go-playground/validator/v10 v10.11.1
github.com/gofiber/fiber/v2 v2.39.0
github.com/golang-jwt/jwt/v4 v4.4.2
github.com/ii64/thrift-idl-builder v0.1.4
github.com/joho/godotenv v1.4.0
go.uber.org/zap v1.23.0
golang.org/x/crypto v0.1.0
gorm.io/driver/mysql v1.4.3
gorm.io/driver/sqlite v1.4.3
gorm.io/gorm v1.24.0
)
require (
github.com/andybalholm/brotli v1.0.4 // indirect
github.com/go-sql-driver/mysql v1.6.0 // indirect
github.com/jinzhu/inflection v1.0.0 // indirect
github.com/jinzhu/now v1.1.5 // indirect
github.com/klauspost/compress v1.15.9 // indirect
github.com/leodido/go-urn v1.2.1 // indirect
github.com/mattn/go-colorable v0.1.13 // indirect
github.com/mattn/go-isatty v0.0.16 // indirect
github.com/mattn/go-runewidth v0.0.14 // indirect
github.com/mattn/go-sqlite3 v1.14.15 // indirect
github.com/pkg/errors v0.9.1 // indirect
github.com/rivo/uniseg v0.2.0 // indirect
github.com/stretchr/testify v1.8.1 // indirect
github.com/valyala/bytebufferpool v1.0.0 // indirect
github.com/valyala/fasthttp v1.41.0 // indirect
github.com/valyala/tcplisten v1.0.0 // indirect
go.uber.org/atomic v1.7.0 // indirect
go.uber.org/multierr v1.6.0 // indirect
golang.org/x/sys v0.1.0 // indirect
golang.org/x/text v0.4.0 // indirect
)

120
go.sum Normal file
View file

@ -0,0 +1,120 @@
github.com/andybalholm/brotli v1.0.4 h1:V7DdXeJtZscaqfNuAdSRuRFzuiKlHSC/Zh3zl9qY3JY=
github.com/andybalholm/brotli v1.0.4/go.mod h1:fO7iG3H7G2nSZ7m0zPUDn85XEX2GTukHGRSepvi9Eig=
github.com/apache/thrift v0.17.0 h1:cMd2aj52n+8VoAtvSvLn4kDC3aZ6IAkBuqWQ2IDu7wo=
github.com/apache/thrift v0.17.0/go.mod h1:OLxhMRJxomX+1I/KUw03qoV3mMz16BwaKI+d4fPBx7Q=
github.com/benbjohnson/clock v1.1.0 h1:Q92kusRqC1XV2MjkWETPvjJVqKetz1OzxZB7mHJLju8=
github.com/creack/pty v1.1.9/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ33E=
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/go-playground/assert/v2 v2.0.1 h1:MsBgLAaY856+nPRTKrp3/OZK38U/wa0CcBYNjji3q3A=
github.com/go-playground/assert/v2 v2.0.1/go.mod h1:VDjEfimB/XKnb+ZQfWdccd7VUvScMdVu0Titje2rxJ4=
github.com/go-playground/locales v0.14.0 h1:u50s323jtVGugKlcYeyzC0etD1HifMjqmJqb8WugfUU=
github.com/go-playground/locales v0.14.0/go.mod h1:sawfccIbzZTqEDETgFXqTho0QybSa7l++s0DH+LDiLs=
github.com/go-playground/universal-translator v0.18.0 h1:82dyy6p4OuJq4/CByFNOn/jYrnRPArHwAcmLoJZxyho=
github.com/go-playground/universal-translator v0.18.0/go.mod h1:UvRDBj+xPUEGrFYl+lu/H90nyDXpg0fqeB/AQUGNTVA=
github.com/go-playground/validator/v10 v10.11.1 h1:prmOlTVv+YjZjmRmNSF3VmspqJIxJWXmqUsHwfTRRkQ=
github.com/go-playground/validator/v10 v10.11.1/go.mod h1:i+3WkQ1FvaUjjxh1kSvIA4dMGDBiPU55YFDl0WbKdWU=
github.com/go-sql-driver/mysql v1.6.0 h1:BCTh4TKNUYmOmMUcQ3IipzF5prigylS7XXjEkfCHuOE=
github.com/go-sql-driver/mysql v1.6.0/go.mod h1:DCzpHaOWr8IXmIStZouvnhqoel9Qv2LBy8hT2VhHyBg=
github.com/gofiber/fiber/v2 v2.39.0 h1:uhWpYQ6EHN8J7FOPYbI2hrdBD/KNZBC5CjbuOd4QUt4=
github.com/gofiber/fiber/v2 v2.39.0/go.mod h1:Cmuu+elPYGqlvQvdKyjtYsjGMi69PDp8a1AY2I5B2gM=
github.com/golang-jwt/jwt/v4 v4.4.2 h1:rcc4lwaZgFMCZ5jxF9ABolDcIHdBytAFgqFPbSJQAYs=
github.com/golang-jwt/jwt/v4 v4.4.2/go.mod h1:m21LjoU+eqJr34lmDMbreY2eSTRJ1cv77w39/MY0Ch0=
github.com/ii64/thrift-idl-builder v0.1.4 h1:hM+9QMCyDrKCUncUxq3gNvbNuQS107GoaVV0XZ+e27E=
github.com/ii64/thrift-idl-builder v0.1.4/go.mod h1:smCsPHGI0sQNUR8YIpg2RzzlSugWjNoY6pyLdmvEoKw=
github.com/ii64/thriftgo v0.2.1-2 h1:bYWxBeyu5EA0yHfevJL+BUGlvdyChvynG48k8zatlQ0=
github.com/ii64/thriftgo v0.2.1-2/go.mod h1:8i9AF5uDdWHGqzUhXDlubCjx4MEfKvWXGQlMWyR0tM4=
github.com/jinzhu/inflection v1.0.0 h1:K317FqzuhWc8YvSVlFMCCUb36O/S9MCKRDI7QkRKD/E=
github.com/jinzhu/inflection v1.0.0/go.mod h1:h+uFLlag+Qp1Va5pdKtLDYj+kHp5pxUVkryuEj+Srlc=
github.com/jinzhu/now v1.1.4/go.mod h1:d3SSVoowX0Lcu0IBviAWJpolVfI5UJVZZ7cO71lE/z8=
github.com/jinzhu/now v1.1.5 h1:/o9tlHleP7gOFmsnYNz3RGnqzefHA47wQpKrrdTIwXQ=
github.com/jinzhu/now v1.1.5/go.mod h1:d3SSVoowX0Lcu0IBviAWJpolVfI5UJVZZ7cO71lE/z8=
github.com/joho/godotenv v1.4.0 h1:3l4+N6zfMWnkbPEXKng2o2/MR5mSwTrBih4ZEkkz1lg=
github.com/joho/godotenv v1.4.0/go.mod h1:f4LDr5Voq0i2e/R5DDNOoa2zzDfwtkZa6DnEwAbqwq4=
github.com/klauspost/compress v1.15.9 h1:wKRjX6JRtDdrE9qwa4b/Cip7ACOshUI4smpCQanqjSY=
github.com/klauspost/compress v1.15.9/go.mod h1:PhcZ0MbTNciWF3rruxRgKxI5NkcHHrHUDtV4Yw2GlzU=
github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo=
github.com/kr/pretty v0.2.1/go.mod h1:ipq/a2n7PKx3OHsz4KJII5eveXtPO4qwEXGdVfWzfnI=
github.com/kr/pretty v0.3.0/go.mod h1:640gp4NfQd8pI5XOwp5fnNeVWj67G7CFk/SaSQn7NBk=
github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ=
github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI=
github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE=
github.com/leodido/go-urn v1.2.1 h1:BqpAaACuzVSgi/VLzGZIobT2z4v53pjosyNd9Yv6n/w=
github.com/leodido/go-urn v1.2.1/go.mod h1:zt4jvISO2HfUBqxjfIshjdMTYS56ZS/qv49ictyFfxY=
github.com/mattn/go-colorable v0.1.13 h1:fFA4WZxdEF4tXPZVKMLwD8oUnCTTo08duU7wxecdEvA=
github.com/mattn/go-colorable v0.1.13/go.mod h1:7S9/ev0klgBDR4GtXTXX8a3vIGJpMovkB8vQcUbaXHg=
github.com/mattn/go-isatty v0.0.16 h1:bq3VjFmv/sOjHtdEhmkEV4x1AJtvUvOJ2PFAZ5+peKQ=
github.com/mattn/go-isatty v0.0.16/go.mod h1:kYGgaQfpe5nmfYZH+SKPsOc2e4SrIfOl2e/yFXSvRLM=
github.com/mattn/go-runewidth v0.0.14 h1:+xnbZSEeDbOIg5/mE6JF0w6n9duR1l3/WmbinWVwUuU=
github.com/mattn/go-runewidth v0.0.14/go.mod h1:Jdepj2loyihRzMpdS35Xk/zdY8IAYHsh153qUoGf23w=
github.com/mattn/go-sqlite3 v1.14.15 h1:vfoHhTN1af61xCRSWzFIWzx2YskyMTwHLrExkBOjvxI=
github.com/mattn/go-sqlite3 v1.14.15/go.mod h1:2eHXhiwb8IkHr+BDWZGa96P6+rkvnG63S2DGjv9HUNg=
github.com/pkg/diff v0.0.0-20210226163009-20ebb0f2a09e/go.mod h1:pJLUxLENpZxwdsKMEsNbx1VGcRFpLqf3715MtcvvzbA=
github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4=
github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
github.com/rivo/uniseg v0.2.0 h1:S1pD9weZBuJdFmowNwbpi7BJ8TNftyUImj/0WQi72jY=
github.com/rivo/uniseg v0.2.0/go.mod h1:J6wj4VEh+S6ZtnVlnTBMWIodfgj8LQOQFoIToxlJtxc=
github.com/rogpeppe/go-internal v1.6.1/go.mod h1:xXDCJY+GAPziupqXw64V24skbSoqbTEfhy4qGm1nDQc=
github.com/rogpeppe/go-internal v1.8.0/go.mod h1:WmiCO8CzOY8rg0OYDC4/i/2WRWAB6poM+XZ2dLUbcbE=
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw=
github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo=
github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI=
github.com/stretchr/testify v1.6.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU=
github.com/stretchr/testify v1.8.1 h1:w7B6lhMri9wdJUVmEZPGGhZzrYTPvgJArz7wNPgYKsk=
github.com/stretchr/testify v1.8.1/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4=
github.com/valyala/bytebufferpool v1.0.0 h1:GqA5TC/0021Y/b9FG4Oi9Mr3q7XYx6KllzawFIhcdPw=
github.com/valyala/bytebufferpool v1.0.0/go.mod h1:6bBcMArwyJ5K/AmCkWv1jt77kVWyCJ6HpOuEn7z0Csc=
github.com/valyala/fasthttp v1.41.0 h1:zeR0Z1my1wDHTRiamBCXVglQdbUwgb9uWG3k1HQz6jY=
github.com/valyala/fasthttp v1.41.0/go.mod h1:f6VbjjoI3z1NDOZOv17o6RvtRSWxC77seBFc2uWtgiY=
github.com/valyala/tcplisten v1.0.0 h1:rBHj/Xf+E1tRGZyWIWwJDiRY0zc1Js+CV5DqwacVSA8=
github.com/valyala/tcplisten v1.0.0/go.mod h1:T0xQ8SeCZGxckz9qRXTfG43PvQ/mcWh7FwZEA7Ioqkc=
go.uber.org/atomic v1.7.0 h1:ADUqmZGgLDDfbSL9ZmPxKTybcoEYHgpYfELNoN+7hsw=
go.uber.org/atomic v1.7.0/go.mod h1:fEN4uk6kAWBTFdckzkM89CLk9XfWZrxpCo0nPH17wJc=
go.uber.org/goleak v1.1.11 h1:wy28qYRKZgnJTxGxvye5/wgWr1EKjmUDGYox5mGlRlI=
go.uber.org/multierr v1.6.0 h1:y6IPFStTAIT5Ytl7/XYmHvzXQ7S3g/IeZW9hyZ5thw4=
go.uber.org/multierr v1.6.0/go.mod h1:cdWPpRnG4AhwMwsgIHip0KRBQjJy5kYEpYjJxpXp9iU=
go.uber.org/zap v1.23.0 h1:OjGQ5KQDEUawVHxNwQgPpiypGHOxo2mNZsOqTak4fFY=
go.uber.org/zap v1.23.0/go.mod h1:D+nX8jyLsMHMYrln8A0rJjFt/T/9/bGgIhAqxv5URuY=
golang.org/x/crypto v0.0.0-20211215153901-e495a2d5b3d3/go.mod h1:IxCIyHEi3zRg3s0A5j5BB6A9Jmi73HwBIUl50j+osU4=
golang.org/x/crypto v0.0.0-20220214200702-86341886e292/go.mod h1:IxCIyHEi3zRg3s0A5j5BB6A9Jmi73HwBIUl50j+osU4=
golang.org/x/crypto v0.1.0 h1:MDRAIl0xIo9Io2xV565hzXHw3zVseKrJKodhohM5CjU=
golang.org/x/crypto v0.1.0/go.mod h1:RecgLatLF4+eUMCP1PoPZQb+cVrJcOPbHkTkbkB9sbw=
golang.org/x/net v0.0.0-20211112202133-69e39bad7dc2/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y=
golang.org/x/net v0.0.0-20220906165146-f3363e06e74c/go.mod h1:YDH+HFinaLZZlnHAfSS6ZXJJ9M9t4Dl22yv3iI2vPwk=
golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20210423082822-04245dca01da/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20210806184541-e5e7981a1069/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20220728004956-3c1f35247d10/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20220811171246-fbc7d0a398ab/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.1.0 h1:kunALQeHf1/185U1i0GOB/fy1IPRDDpuoOOqRReG57U=
golang.org/x/sys v0.1.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8=
golang.org/x/text v0.3.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ=
golang.org/x/text v0.4.0 h1:BrVqGRd7+k1DiOgtnFvAkoQEWQvBc25ouMJM6429SFg=
golang.org/x/text v0.4.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8=
golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q=
gopkg.in/errgo.v2 v2.1.0/go.mod h1:hNsd1EY+bozCKY1Ytp96fpM3vjJbqLJn88ws8XvfDNI=
gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
gopkg.in/yaml.v3 v3.0.0-20210107192922-496545a6307b/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
gorm.io/driver/mysql v1.4.3 h1:/JhWJhO2v17d8hjApTltKNADm7K7YI2ogkR7avJUL3k=
gorm.io/driver/mysql v1.4.3/go.mod h1:sSIebwZAVPiT+27jK9HIwvsqOGKx3YMPmrA3mBJR10c=
gorm.io/driver/sqlite v1.4.3 h1:HBBcZSDnWi5BW3B3rwvVTc510KGkBkexlOg0QrmLUuU=
gorm.io/driver/sqlite v1.4.3/go.mod h1:0Aq3iPO+v9ZKbcdiz8gLWRw5VOPcBOPUQJFLq5e2ecI=
gorm.io/gorm v1.23.8/go.mod h1:l2lP/RyAtc1ynaTjFksBde/O8v9oOGIApu2/xRitmZk=
gorm.io/gorm v1.24.0 h1:j/CoiSm6xpRpmzbFJsQHYj+I8bGYWLXVHeYEyyKlF74=
gorm.io/gorm v1.24.0/go.mod h1:DVrVomtaYTbqs7gB/x2uVvqnXzv0nqjB396B8cG4dBA=

View file

@ -0,0 +1,7 @@
namespace go biz.core.exceptions
exception CoreServicesException {
1: i32 code
2: string message
3: map<string, string> parameters
}

View file

@ -0,0 +1,71 @@
include "./structs.thrift"
include "./exceptions.thrift"
namespace go biz.core
service CoreService {
// oneway void asd()
structs.LoginResponse login(
1: structs.LoginRequest request
) throws (1: exceptions.CoreServicesException e)
//@authenticated
structs.GetProfileResponse getProfile(
1: structs.GetProfileRequest request
) throws (1: exceptions.CoreServicesException e)
//@authenticated
structs.LogoutResponse logout(
1: structs.LogoutRequest request
) throws (1: exceptions.CoreServicesException e)
//@authenticated
//@role(Admin)
// Get list of users registered in system
structs.GetUserListResponse getUserList(
1: structs.GetUserListRequest request
) throws (1: exceptions.CoreServicesException e)
//@authenticated
//@role(Admin)
structs.CreateUserResponse createUser(
1: structs.CreateUserRequest request
) throws (1: exceptions.CoreServicesException e)
//@authenticated
//@role(Admin)
structs.DeleteUsersResponse deleteUsers(
1: structs.DeleteUsersRequest request,
) throws (1: exceptions.CoreServicesException e)
//@authenticated
//@role(All)
// Get list of accessible WajibPajak by OWNED|ROLE
structs.GetWajibPajakListResponse getWajibPajakList(
1: structs.GetWajibPajakListRequest request
) throws (1: exceptions.CoreServicesException e)
//@authenticated
//@role(All)
// Create WajibPajak
structs.CreateWajibPajakResponse createWajibPajak(
1: structs.CreateWajibPajakRequest request
) throws (1: exceptions.CoreServicesException e)
//@authenticated
//@role(All)
structs.DeleteWajibpajakListResponse deleteWajibPajakList(
1: structs.DeleteWajibpajakListRequest request
) throws (1: exceptions.CoreServicesException e)
}

200
idl/biz/core/structs.thrift Normal file
View file

@ -0,0 +1,200 @@
namespace go biz.core.structs
// WARNING WARNING WARNING WARNING WARNING WARNING WARNING
// Please don't mix DTO and DAO !
// This design is intended to be as-is.
// WARNING WARNING WARNING WARNING WARNING WARNING WARNING
enum RoleType {
UNKNOWN = 0
SYSTEM = 1
USER = 2
WP_OWNER = 100
WP_ADMIN = 110
}
struct Role {
1: i64 id (go.tag = "gorm:\"primaryKey\"")
5: string displayName (go.tag = "core_v1_dto:\"required\"")
6: RoleType roleType (go.tag = "core_v1_dto:\"required\"")
// user that create this role
39: optional User creator (go.tag = "gorm:\"foreignKey:id;references:id;\"")
// users with this role
40: optional list<User> users (go.tag = "gorm:\"many2many:user_roles;foreignKey:id;references:id;\"")
// wajib pajak bound with this role
41: optional list<WajibPajak> wajibPajakList (go.tag = "gorm:\"many2many:wp_roles;foreignKey:id;references:id\"")
}
//@direct
struct User {
1: i64 id (go.tag = "gorm:\"primaryKey\"")
2: string username (go.tag = "core_v1_dto:\"required\" gorm:\"index\"")
3: string password (go.tag = "core_v1_dto:\"required\"")
4: string displayName (go.tag = "core_v1_dto:\"required\"")
60: binary privateKey
61: binary publicKey
// system roles
40: optional list<Role> roles (go.tag = "gorm:\"many2many:user_roles;foreignKey:id;references:id;\"")
// wp owned by user
45: optional list<WajibPajak> ownedWajibPajakList (go.tag = "gorm:\"many2many:user_wps;foreignKey:id;references:id;constraint:OnDelete:CASCADE\"")
// wp roles roles owned by user
50: optional list<Role> rolesWajibPajakList (go.tag = "gorm:\"many2many:wp_roles;foreignKey:id;references:id;\"")
}
//@direct
// struct JenisPajakXX {
// 1: i64 id (go.tag = "gorm:\"primaryKey\"")
// 2: string displayName (go.tag = "core_v1_dto:\"required\"")
// }
enum JenisPajak {
UNKNOWN = 0
PPH_21 = 1
PPH_23 = 2
PPH_25 = 3
PPH_26 = 4
PPH_4_2 = 5
PPH_15 = 6
PPN = 7
TAHUNAN = 8
}
struct WajibPajakProfile {
// 1: i64 id (go.tag = "gorm:\"primaryKey\"")
2: string npwp (go.tag = "core_v1_dto:\"required\"")
3: string displayName (go.tag = "core_v1_dto:\"required\"")
4: string address (go.tag = "core_v1_dto:\"required\"")
}
struct WajibPajakTaxObligation {
1: i64 id (go.tag = "gorm:\"primaryKey\"")
2: JenisPajak obligation (go.tag = "gorm:\"foreignKey:id;references:id\"")
3: bool isActive (go.tag = "")
}
//@direct
struct WajibPajak {
1: i64 id (go.tag = "gorm:\"primaryKey\"")
2: WajibPajakProfile profile (go.tag = "gorm:\"embedded;embeddedPrefix:profile_;constraint:OnDelete:CASCADE\" core_v1_dto:\"required\"")
// wajib pajak owner
30: list<User> owners (go.tag = "gorm:\"many2many:wp_roles;foreignKey:id;references:id;\"")
// wajib pajak tax obligations
40: optional list<WajibPajakTaxObligation> taxObligations (go.tag = "gorm:\"many2many:wp_obligations;foreignKey:id;references:id;\"")
// role bound to this WajibPajak
45: optional list<Role> roles (go.tag = "gorm:\"many2many:wp_roles;foreignKey:id;references:id;constraint:OnDelete:CASCADE\"")
}
// ----------------------------------------------------------------
struct Pagination {
1: i64 page
2: i64 rowsPerPage
}
struct AlertInfo {
1: string title
2: string description
}
// ----------------------------------------------------------------
struct LoginRequest {
1: string username (go.tag = "core_v1_dto:\"required\"")
2: string password (go.tag = "core_v1_dto:\"required\"")
}
struct LoginResponse {
1: string trkToken
2: string token
10: optional User profile
}
struct LogoutRequest {
1: string token (go.tag = "core_v1_dto:\"required\"")
}
struct LogoutResponse {
}
struct GetProfileRequest {
}
struct GetProfileResponse {
2: User profile
}
enum WajibPajakOwnership {
UNKNOWN = 0
OWNED = 1
ROLE = 2
}
struct GetUserListRequest {
1: Pagination pagination
3: string searchTerm
}
struct GetUserListResponse {
1: Pagination pagination
2: i64 totalUsers
3: list<User> users
}
struct CreateUserRequest {
1: User user
}
struct CreateUserResponse {
}
struct DeleteUsersRequest {
1: list<i64> userIds
}
struct DeleteUsersResponse {
1: list<i64> success
2: list<i64> ignored
}
struct GetWajibPajakListRequest {
1: Pagination pagination
2: WajibPajakOwnership ownership
3: string searchTerm
}
struct GetWajibPajakListResponse {
1: Pagination pagination
2: i64 totalWajibPajak
3: list<WajibPajak> wajibPajakList
}
struct CreateWajibPajakRequest {
1: WajibPajak wajibPajak (go.tag = "core_v1_dto:\"required\"")
}
struct CreateWajibPajakResponse {
}
struct DeleteWajibpajakListRequest {
1: list<i64> wpIds
}
struct DeleteWajibpajakListResponse {
1: list<i64> success
2: list<i64> ignored
}

View file

@ -0,0 +1,7 @@
namespace go common.exceptionsc
exception CommonException {
1: i32 code
2: string message
3: optional map<string, string> metadata
}

View file

@ -0,0 +1,3 @@
include "./exceptionsc.thrift"
namespace go common

383
internal/biz/core/core.go Normal file
View file

@ -0,0 +1,383 @@
package coresvc
import (
"context"
"crypto/ed25519"
"crypto/rand"
"encoding/gob"
"encoding/json"
"errors"
"fmt"
"strings"
"time"
"wpw-common/internal"
"wpw-common/internal/http"
"wpw-common/internal/vdext"
"wpw-common/pkg/gen/biz/core"
"wpw-common/pkg/gen/biz/core/exceptions"
"wpw-common/pkg/gen/biz/core/structs"
"github.com/go-playground/validator/v10"
"github.com/gofiber/fiber/v2/middleware/session"
"github.com/golang-jwt/jwt/v4"
"go.uber.org/zap"
"gorm.io/gorm"
)
type scopeFunc = func(db *gorm.DB) *gorm.DB
type CoreService struct {
vd *validator.Validate
logger *zap.SugaredLogger
db *gorm.DB
secret []byte
authExpireDur time.Duration
}
func init() {
gob.Register(&structs.User{})
}
var _ core.CoreService = (*CoreService)(nil)
type CoreV1ServiceParams struct {
Logger *zap.Logger
DB *gorm.DB
Secret []byte
}
func NewCoreServiceFromAppContext(appCtx *internal.AppContext) *CoreService {
var (
argName string
db, secret any
exist bool
)
// db
db, exist = appCtx.Data["db"]
if !exist {
argName = "db"
goto invalid_arguments
}
// secret
secret, exist = appCtx.Data["secret"]
if !exist {
argName = "secret"
goto invalid_arguments
}
return NewCoreService(CoreV1ServiceParams{
Logger: appCtx.Logger,
DB: db.(*gorm.DB),
Secret: []byte(secret.(string)),
})
invalid_arguments:
appCtx.Logger.Sugar().Fatal("core v1 service requires `%s` on application context", argName)
return nil
}
func NewCoreService(params CoreV1ServiceParams) *CoreService {
c := &CoreService{
vd: vdext.New("core_v1_dto"),
logger: params.Logger.Sugar().Named("core-v1"),
db: params.DB,
secret: params.Secret,
authExpireDur: time.Hour * 5,
}
token := jwt.New(jwt.SigningMethodEdDSA)
claims := token.Claims.(jwt.MapClaims)
_ = claims
c.migrateSchemeAndData()
return c
}
func (c *CoreService) getEd25519KeyPair() (pri ed25519.PrivateKey, pub ed25519.PublicKey, err error) {
pub, pri, err = ed25519.GenerateKey(rand.Reader)
return
}
func (c *CoreService) generateJwtTokenForUser(user *structs.User, jti string) (tokenStr string, err error) {
// user.PrivateKey
token := jwt.New(jwt.SigningMethodEdDSA)
claims := token.Claims.(jwt.MapClaims)
claims["exp"] = time.Now().Add(c.authExpireDur)
claims["iss"] = "core-v1"
claims["uid"] = user.ID
claims["jti"] = jti
claims["username"] = user.Username
var roleIds []int64
for _, role := range user.Roles {
roleIds = append(roleIds, role.ID)
}
claims["roles"] = roleIds
tokenStr, err = token.SignedString(ed25519.PrivateKey(user.PrivateKey))
return
}
func (c *CoreService) stripConfidentialUserData(u *structs.User) {
u.Password = ""
u.PrivateKey = nil
u.PublicKey = nil
}
func (c *CoreService) getSession(ctx context.Context) (sess *session.Session, err error) {
fctx := http.GetFiberFromContext(ctx)
sess, err = http.Session.Get(fctx)
if err != nil {
c.logger.Error("get session failed: ", err)
exc := exceptions.NewCoreServicesException()
exc.Code = 500
exc.Message = "unable to get session"
exc.Parameters = map[string]string{
"inner": err.Error(),
}
err = exc
return
}
if authToken := fctx.Request().Header.Peek("X-Hub-Auth-Token"); authToken != nil {
// tbd
}
return
}
var (
ErrBadRequest = errors.New("bad request")
ErrForbidden = errors.New("forbidden")
ErrNotAuthenticated = errors.New("not authenticated")
ErrInvalidState = errors.New("invalid state")
)
func (c *CoreService) checkSession(sess *session.Session) (user *structs.User, err error) {
userVal := sess.Get("user")
if userVal == nil {
err = ErrNotAuthenticated
return
}
var ok bool
user, ok = userVal.(*structs.User)
if !ok {
err = ErrNotAuthenticated
return
}
tx := c.db.Where("id = ?", user.ID).First(user)
if err = tx.Error; err != nil {
// must check if the user is still valid / no db error
// or we just revoke the session.
if err = sess.Destroy(); err != nil {
return
}
return
}
// TODO: also use the JWT ?
return
}
func (c *CoreService) getAndCheckSession(ctx context.Context) (user *structs.User, err error) {
var sess *session.Session
if sess, err = c.getSession(ctx); err != nil {
return
}
if user, err = c.checkSession(sess); err != nil {
return
}
return
}
func (c *CoreService) getAndCheckSessionAdmin(ctx context.Context) (user *structs.User, err error) {
if user, err = c.getAndCheckSession(ctx); err != nil {
return
}
// user with type system
if !c.isSystemUser(user) {
err = ErrForbidden
return
}
return
}
func (c *CoreService) isSystemUser(user *structs.User) bool {
for _, role := range user.Roles {
if role.RoleType == structs.RoleType_SYSTEM {
return true
}
}
return false
}
func (c *CoreService) alert2Str(alert *structs.AlertInfo) (s string) {
var b []byte
var err error
if b, err = json.Marshal(alert); err != nil {
s = "{}"
return
}
s = string(b)
return
}
func (c *CoreService) wrapServiceError(err error, customMessage ...string) error {
if errVal, ok := err.(*exceptions.CoreServicesException); ok {
return errVal
}
exc := exceptions.NewCoreServicesException()
exc.Message = err.Error()
switch err {
case gorm.ErrRecordNotFound:
exc.Code = 404
alert := &structs.AlertInfo{
Title: "Resource",
Description: "Resource not found",
}
if len(customMessage) >= 2 {
alert.Title = customMessage[0]
alert.Description = customMessage[1]
} else if len(customMessage) >= 1 {
alert.Description = customMessage[0]
}
exc.Parameters = map[string]string{
"alert": c.alert2Str(alert),
}
case ErrForbidden:
exc.Code = 403
exc.Parameters = map[string]string{
"alert": c.alert2Str(&structs.AlertInfo{
Title: "Access Error",
Description: err.Error(),
}),
}
case ErrBadRequest:
exc.Code = 400
exc.Parameters = map[string]string{
"alert": c.alert2Str(&structs.AlertInfo{
Title: "Input Error",
Description: err.Error(),
}),
}
case ErrNotAuthenticated:
exc.Code = 401
exc.Parameters = map[string]string{
"alert": c.alert2Str(&structs.AlertInfo{
Title: "Auth Error",
Description: err.Error(),
}),
}
case ErrInvalidState:
exc.Code = 500
exc.Parameters = map[string]string{
"redirect": "/",
"alert": c.alert2Str(&structs.AlertInfo{
Title: "Server Error",
Description: err.Error(),
}),
}
default:
exc.Code = 500
exc.Parameters = map[string]string{
"alert": c.alert2Str(&structs.AlertInfo{
Title: "Server Error",
Description: err.Error(),
}),
}
}
return exc
}
func (c *CoreService) checkRequest(ctx context.Context, v any) (err error) {
err = c.vd.StructCtx(ctx, v)
if err != nil {
exc := exceptions.NewCoreServicesException()
exc.Code = 400
exc.Message = err.Error()
exc.Parameters = map[string]string{
"alert": c.alert2Str(&structs.AlertInfo{
Title: "Validation Error",
Description: c.err2str(err),
}),
}
err = exc
return
}
return
}
func (c *CoreService) passwordHasher(plain string) string {
return passwordHasher(plain, c.secret)
}
func (c *CoreService) err2str(err error) (s string) {
var ve validator.ValidationErrors
if errors.As(err, &ve) {
var sb strings.Builder
for i, f := range ve {
sb.WriteString(f.Field())
sb.Write([]byte(` `))
sb.WriteString(f.Tag())
if i+1 < len(ve) {
sb.Write([]byte(`, `))
}
}
return sb.String()
}
return err.Error()
}
// pagination2Offset correct pagination and return the offset
func (c *CoreService) pagination2Offset(pagination *structs.Pagination) (offset int, limit int) {
offset, limit = 0, 25
if pagination == nil {
return
}
if pagination.Page <= 0 {
pagination.Page = 1
}
switch {
case pagination.RowsPerPage > 100:
pagination.RowsPerPage = 100
case pagination.RowsPerPage <= 0:
pagination.RowsPerPage = int64(limit)
}
limit = int(pagination.RowsPerPage)
offset = int(pagination.Page-1) * limit
return
}
// ScPaginate gorm scope function
func (c *CoreService) ScPaginate(pagination *structs.Pagination) scopeFunc {
return func(db *gorm.DB) *gorm.DB {
offset, limit := c.pagination2Offset(pagination)
// use id > ? next time.
return db.Limit(limit).Offset(offset)
}
}
// ScSearchTerm gorm scope function
func (c *CoreService) ScSearchTerm(searchTerm string, fields ...string) scopeFunc {
// bloat FTS-like search :(
var sb strings.Builder
var values []any
if len(fields) <= 0 {
goto ret
}
for i, field := range fields {
fmt.Fprintf(&sb, "%s LIKE ?", field)
if i < len(fields)-1 {
sb.WriteString(" OR ")
}
values = append(values, fmt.Sprintf("%%%s%%", searchTerm))
}
ret:
return func(db *gorm.DB) *gorm.DB {
return db.Where(sb.String(), values...)
}
}

View file

@ -0,0 +1,94 @@
package coresvc
import (
"fmt"
"wpw-common/internal/system"
"wpw-common/pkg/gen/biz/core/structs"
"gorm.io/gorm"
)
func (c *CoreService) migrateDefaultData() [][]any {
mustNot := func(err error) {
if err != nil {
c.logger.Fatalw("must no error", "err", err)
}
}
// user admin
priUser1, pubUser1, err := c.getEd25519KeyPair()
mustNot(err)
var testUser []any
for i := 0; i < 100; i++ {
testUser = append(testUser, &structs.User{
Username: fmt.Sprintf("psw_user%d", i),
DisplayName: fmt.Sprintf("The User %d", i),
Roles: []*structs.Role{},
})
}
var testWp []any
for i := 0; i < 200; i++ {
testWp = append(testWp, &structs.WajibPajak{
Profile: &structs.WajibPajakProfile{
// not validated.
Npwp: fmt.Sprintf("%15d", i),
DisplayName: fmt.Sprintf("PT. Wajib Pajak %d Tbk.", i),
Address: fmt.Sprintf("Lorem Ipsum %d, Surabaya, East Java", i),
},
Owners: []*structs.User{testUser[i/2].(*structs.User)},
})
}
return [][]any{
// User
append([]any{
&structs.User{
Username: "admin",
Password: c.passwordHasher("admin512"),
DisplayName: "The Admin",
Roles: []*structs.Role{
{ID: 1, DisplayName: "admin", RoleType: structs.RoleType_SYSTEM},
{ID: 2, DisplayName: "operator", RoleType: structs.RoleType_SYSTEM},
},
PrivateKey: priUser1,
PublicKey: pubUser1,
},
}, testUser...),
// // Wajib Pajak
append([]any{
// &structs.WajibPajak{},
}, testWp...),
}
}
func (c *CoreService) migrateSchemeAndData() {
system.Default.DoOnce("core_v1_migrate", func() error {
c.logger.Info("migrate scheme and data")
err := c.db.Transaction(func(tx *gorm.DB) (err error) {
for _, migKinds := range c.migrateDefaultData() {
if len(migKinds) <= 0 {
continue
}
if err = tx.AutoMigrate(migKinds[0]); err != nil {
return
}
for _, mData := range migKinds {
if err = tx.Create(mData).Error; err != nil {
return
}
}
}
return
})
// fail immediately if the migration fails
if err != nil {
c.logger.Fatalw("failed to migrate",
"err", err)
}
return nil
})
}

View file

@ -0,0 +1,117 @@
package coresvc
import (
"context"
"wpw-common/pkg/gen/biz/core"
"wpw-common/pkg/gen/biz/core/structs"
"github.com/gofiber/fiber/v2/middleware/session"
)
var _ core.CoreService = (*CoreService)(nil)
func (c *CoreService) Login(ctx context.Context, request *structs.LoginRequest) (r *structs.LoginResponse, err error) {
c.logger.Infow("login request", "req", request)
// alert := &structs.AlertInfo{}
// get session
var sess *session.Session
if sess, err = c.getSession(ctx); err != nil {
return
}
var userSession *structs.User
if userSession, err = c.checkSession(sess); err == ErrNotAuthenticated {
// login method don't need to be authenticated.
goto enter
} else if userSession != nil {
err = c.wrapServiceError(ErrInvalidState)
return
} else if err != nil {
err = c.wrapServiceError(err)
return
}
enter:
// validate the request
if err = c.checkRequest(ctx, request); err != nil {
return
}
// hash user psw input
var cipPsw = c.passwordHasher(request.Password)
var user structs.User
user.Username = request.Username
user.Password = cipPsw
// lookup for username and hashed psw input
tx := c.db.Where(&user).Preload("Roles").First(&user)
if err = tx.Error; err != nil {
err = c.wrapServiceError(err, "Login Error", "Username or password is incorrect")
return
}
r = structs.NewLoginResponse()
r.TrkToken = sess.ID()
// generate jwt token
if r.Token, err = c.generateJwtTokenForUser(&user, sess.ID()); err != nil {
err = c.wrapServiceError(err)
return
}
// strip confidental user data
c.stripConfidentialUserData(&user)
r.Profile = &user
// send track session token
sess.Set("user", &user)
if err = sess.Save(); err != nil {
err = c.wrapServiceError(err)
return
}
return
}
func (c *CoreService) Logout(ctx context.Context, request *structs.LogoutRequest) (r *structs.LogoutResponse, err error) {
c.logger.Infow("logout request", "req", request)
// get session
var sess *session.Session
if sess, err = c.getSession(ctx); err != nil {
return
}
// check session
var user *structs.User
if user, err = c.checkSession(sess); err != nil {
err = c.wrapServiceError(err)
return
}
c.logger.Infow("user logout", "user", user)
if err = sess.Destroy(); err != nil {
err = c.wrapServiceError(err)
return
}
r = structs.NewLogoutResponse()
return
}
func (c *CoreService) GetProfile(ctx context.Context, request *structs.GetProfileRequest) (r *structs.GetProfileResponse, err error) {
c.logger.Infow("get profile request", "req", request)
var sess *session.Session
if sess, err = c.getSession(ctx); err != nil {
return
}
var user *structs.User
if user, err = c.checkSession(sess); err != nil {
err = c.wrapServiceError(err)
return
}
r = structs.NewGetProfileResponse()
r.Profile = user
return
}

View file

@ -0,0 +1,160 @@
package coresvc
import (
"context"
"crypto/ed25519"
"wpw-common/pkg/gen/biz/core"
"wpw-common/pkg/gen/biz/core/structs"
"gorm.io/gorm"
)
var _ core.CoreService = (*CoreService)(nil)
func (c *CoreService) GetUserList(ctx context.Context, request *structs.GetUserListRequest) (r *structs.GetUserListResponse, err error) {
c.logger.Infow("get users request", "req", request)
var user *structs.User
if user, err = c.getAndCheckSessionAdmin(ctx); err != nil {
err = c.wrapServiceError(err)
return
}
_ = user
if err = c.checkRequest(ctx, request); err != nil {
return
}
r = structs.NewGetUserListResponse()
r.Pagination = request.Pagination
// query scopes
scopes := []scopeFunc{
c.ScPaginate(request.Pagination),
}
// using search term
if searchTerm := request.SearchTerm; searchTerm != "" {
scopes = append(scopes, c.ScSearchTerm(request.SearchTerm,
"username", "display_name"))
}
// get users
tx := c.db.Scopes(scopes...).
Preload("Roles").
Find(&r.Users)
if err = tx.Error; err != nil {
err = c.wrapServiceError(err)
return
}
// get total users count
tx = c.db.Model(structs.User{}).Count(&r.TotalUsers)
if err = tx.Error; err != nil {
err = c.wrapServiceError(err)
return
}
// strip users data
for _, user := range r.Users {
c.stripConfidentialUserData(user)
}
return
}
func (c *CoreService) CreateUser(ctx context.Context, request *structs.CreateUserRequest) (r *structs.CreateUserResponse, err error) {
c.logger.Infow("create user request", "req", request)
var user *structs.User
if user, err = c.getAndCheckSessionAdmin(ctx); err != nil {
return
}
_ = user
// validate
requestUser := request.User
if err = c.checkRequest(ctx, requestUser); err != nil {
return
}
daoUser := structs.NewUser()
// pass necessary data.
daoUser.Username = requestUser.Username
daoUser.Password = c.passwordHasher(requestUser.Password)
daoUser.DisplayName = requestUser.DisplayName
var (
pri ed25519.PrivateKey
pub ed25519.PublicKey
)
if pri, pub, err = c.getEd25519KeyPair(); err != nil {
err = c.wrapServiceError(err)
return
}
daoUser.PrivateKey = pri
daoUser.PublicKey = pub
tx := c.db.Create(daoUser)
if err = tx.Error; err != nil {
err = c.wrapServiceError(err)
return
}
r = structs.NewCreateUserResponse()
return
}
func (c *CoreService) DeleteUsers(ctx context.Context, request *structs.DeleteUsersRequest) (r *structs.DeleteUsersResponse, err error) {
c.logger.Infow("delete users request", "req", request)
var user *structs.User
if user, err = c.getAndCheckSessionAdmin(ctx); err != nil {
return
}
_ = user
if err = c.checkRequest(ctx, request); err != nil {
return
}
var targetUsers []*structs.User
tx := c.db.
Where("id IN ?", request.UserIds).Select("id").
Preload("Roles").
Find(&targetUsers)
if err = tx.Error; err != nil {
err = c.wrapServiceError(err)
return
}
var qualifiedTargetUsers []*structs.User
var (
idsSuccess []int64
idsIgnored []int64
)
err = c.db.Transaction(func(tx *gorm.DB) (err error) {
for _, targetUser := range targetUsers {
if targetUser.ID == user.ID || c.isSystemUser(targetUser) {
idsIgnored = append(idsIgnored, targetUser.ID)
continue
}
qualifiedTargetUsers = append(qualifiedTargetUsers, targetUser)
idsSuccess = append(idsSuccess, targetUser.ID)
}
if len(qualifiedTargetUsers) <= 0 {
// has nothing to do with.
return
}
tx = tx.Delete(qualifiedTargetUsers)
if err = tx.Error; err != nil {
return
}
return
})
if err != nil {
err = c.wrapServiceError(err)
return
}
r = structs.NewDeleteUsersResponse()
r.Success = idsSuccess
r.Ignored = idsIgnored
return
}

View file

@ -0,0 +1,181 @@
package coresvc
import (
"context"
"wpw-common/pkg/gen/biz/core"
"wpw-common/pkg/gen/biz/core/structs"
"gorm.io/gorm"
)
var _ core.CoreService = (*CoreService)(nil)
func (c *CoreService) GetWajibPajakList(ctx context.Context, request *structs.GetWajibPajakListRequest) (r *structs.GetWajibPajakListResponse, err error) {
c.logger.Infow("get wajib pajak request", "req", request)
var user *structs.User
if user, err = c.getAndCheckSession(ctx); err != nil {
return
}
_ = user
if err = c.checkRequest(ctx, request); err != nil {
return
}
r = structs.NewGetWajibPajakListResponse()
r.Pagination = request.Pagination
// query scopes
scopes := []scopeFunc{
c.ScPaginate(request.Pagination),
}
// using search term
if searchTerm := request.SearchTerm; searchTerm != "" {
scopes = append(scopes, c.ScSearchTerm(request.SearchTerm,
"profile_npwp", "profile_display_name", "profile_address"))
}
// admin user, ignoring the `ownership`
if c.isSystemUser(user) {
// get all wps
tx := c.db.Scopes(scopes...).
Find(&r.WajibPajakList)
if err = tx.Error; err != nil {
return
}
// get all wps count
tx = c.db.Model(structs.WajibPajak{}).Count(&r.TotalWajibPajak)
if err = tx.Error; err != nil {
return
}
return
}
// normal user
var tx *gorm.DB
switch request.Ownership {
case structs.WajibPajakOwnership_OWNED:
tx = c.db.
Preload("OwnedWajibPajakList").
Where(user).
First(&user)
if err = tx.Error; err != nil {
err = c.wrapServiceError(err)
return
}
r.WajibPajakList = user.OwnedWajibPajakList
r.TotalWajibPajak = int64(len(user.OwnedWajibPajakList)) // TODO: temporary
case structs.WajibPajakOwnership_ROLE:
tx = c.db.
Preload("RolesWajibPajakList").
Preload("RolesWajibPajakList.WajibPajakList").
Where(user).First(&user)
if err = tx.Error; err != nil {
err = c.wrapServiceError(err)
return
}
for _, role := range user.Roles {
r.WajibPajakList = append(r.WajibPajakList, role.WajibPajakList...)
}
r.TotalWajibPajak = int64(len(r.WajibPajakList)) // TODO: temporary
default:
err = c.wrapServiceError(ErrBadRequest)
return
}
return
}
func (c *CoreService) CreateWajibPajak(ctx context.Context, request *structs.CreateWajibPajakRequest) (r *structs.CreateWajibPajakResponse, err error) {
c.logger.Infow("create wajib pajajk request", "req", request)
var user *structs.User
if user, err = c.getAndCheckSession(ctx); err != nil {
return
}
_ = user
if err = c.checkRequest(ctx, request); err != nil {
return
}
var wp structs.WajibPajak
wp.Profile = request.WajibPajak.Profile
wp.Owners = []*structs.User{
// set authenticated user as the ownner of the Wajib Pajak
user,
}
tx := c.db.Create(&wp)
if err = tx.Error; err != nil {
err = c.wrapServiceError(err)
return
}
r = structs.NewCreateWajibPajakResponse()
return
}
func (c *CoreService) DeleteWajibPajakList(ctx context.Context, request *structs.DeleteWajibpajakListRequest) (r *structs.DeleteWajibpajakListResponse, err error) {
c.logger.Infow("delete wajib pajak request", "req", request)
var user *structs.User
if user, err = c.getAndCheckSession(ctx); err != nil {
return
}
_ = user
if err = c.checkRequest(ctx, request); err != nil {
return
}
var targetWps []*structs.WajibPajak
tx := c.db.
Where("id IN ?", request.WpIds).
Select("id").
Preload("Owners").
Find(&targetWps)
if err = tx.Error; err != nil {
err = c.wrapServiceError(err)
return
}
var (
qsTargetWps []*structs.WajibPajak
idsSuccess []int64
idsIgnored []int64
)
err = c.db.Transaction(func(tx *gorm.DB) (err error) {
for _, targetWp := range targetWps {
var found bool
for _, owner := range targetWp.Owners {
if owner.ID == user.ID {
qsTargetWps = append(qsTargetWps, targetWp)
found = true
break
}
}
if found {
idsSuccess = append(idsSuccess, targetWp.ID)
} else {
idsIgnored = append(idsIgnored, targetWp.ID)
}
}
tx = tx.Delete(qsTargetWps)
if err = tx.Error; err != nil {
err = c.wrapServiceError(err)
return
}
return
})
if err != nil {
err = c.wrapServiceError(err)
return
}
r = structs.NewDeleteWajibpajakListResponse()
r.Success = idsSuccess
r.Ignored = idsIgnored
return
}

18
internal/biz/core/util.go Normal file
View file

@ -0,0 +1,18 @@
package coresvc
import (
"crypto/sha256"
"encoding/base64"
"golang.org/x/crypto/pbkdf2"
)
var (
pswIter = 2 << 11
pswLength = 50 // raw length
)
func passwordHasher(plain string, salt []byte) string {
psw := pbkdf2.Key([]byte(plain), salt, pswIter, pswLength, sha256.New)
return base64.StdEncoding.EncodeToString(psw)
}

84
internal/context.go Normal file
View file

@ -0,0 +1,84 @@
package internal
import (
"context"
"sync"
"sync/atomic"
"go.uber.org/zap"
)
type contextKey string
var (
contextKeyApp contextKey = "application-context"
)
func WithAppContext(ctx context.Context, appCtx *AppContext) context.Context {
appCtx.parent = ctx
return context.WithValue(ctx, contextKeyApp, appCtx)
}
func GetAppContext(ctx context.Context) *AppContext {
return ctx.Value(contextKeyApp).(*AppContext)
}
type AppContext struct {
parent context.Context
Logger *zap.Logger
Data map[string]any
Cancel context.CancelFunc
task []task
taskCnt atomic.Int32
taskDone atomic.Int32
wg sync.WaitGroup
}
func (c AppContext) Init() AppContext {
c.Data = make(map[string]any)
return c
}
func (c *AppContext) GetContext() context.Context {
ctx, cancel := context.WithCancel(c.parent)
c.task = append(c.task, task{
ctx: ctx,
cancel: cancel,
})
return ctx
}
// DoTask spawn task on a goroutine
func (p *AppContext) DoTask(f func(ctx context.Context)) {
ctx := p.GetContext()
p.wg.Add(1)
p.taskCnt.Add(1)
go func() {
defer p.wg.Done()
defer p.taskDone.Add(1)
f(ctx)
}()
}
func (p *AppContext) TaskCount() int {
return int(p.taskCnt.Load())
}
func (p *AppContext) TaskDone() int {
return int(p.taskDone.Load())
}
func (p *AppContext) TaskRemaining() int {
return p.TaskCount() - p.TaskDone()
}
func (p *AppContext) WaitTask() {
p.wg.Wait()
}
type task struct {
ctx context.Context
cancel context.CancelFunc
}

167
internal/http/server.go Normal file
View file

@ -0,0 +1,167 @@
package http
import (
"bytes"
"context"
"mime"
"net/http"
"strings"
"time"
"wpw-common/internal"
"wpw-common/public"
"github.com/apache/thrift/lib/go/thrift"
"github.com/gofiber/fiber/v2"
"github.com/gofiber/fiber/v2/middleware/cors"
"github.com/gofiber/fiber/v2/middleware/filesystem"
"github.com/gofiber/fiber/v2/middleware/recover"
"github.com/gofiber/fiber/v2/middleware/session"
"go.uber.org/zap"
)
var (
defaultServer = newServer()
Session = session.New(session.Config{
KeyLookup: "header:X-Hub-Track-Token",
})
thriftContentType = []byte("application/x-thrift")
contextKeyFiber contextKey = "fiber-app"
hubHeader = []string{
"X-Hub-Env",
"X-Hub-App",
"X-Hub-Version",
"X-Hub-App",
"X-Hub-Track-Token",
"X-Hub-Auth-Token",
}
)
type contextKey string
type server struct {
pCtx context.Context
app *fiber.App
logger *zap.SugaredLogger
}
func newServer() *server {
app := fiber.New()
app.Use(recover.New())
app.Use(cors.New(cors.Config{
// AllowOrigins: "https://gofiber.io, https://gofiber.net",
AllowOrigins: "*",
AllowHeaders: strings.Join(append([]string{
"Origin",
"Content-Type",
"Accept",
}, hubHeader...), ", "),
ExposeHeaders: strings.Join(hubHeader, ", "),
}))
app.Use(filesystem.New(filesystem.Config{
Root: http.FS(public.FS),
Browse: false,
Index: "index.html",
MaxAge: 3000,
NotFoundFile: "index.html",
}))
srv := &server{app: app}
return srv
}
func (s *server) registerService(path string, p thrift.TProcessor) {
processorFactory := thrift.NewTProcessorFactory(p)
protocolFactory := thrift.NewTCompactProtocolFactoryConf(&thrift.TConfiguration{})
contentTypeResp := mime.FormatMediaType(string(thriftContentType), map[string]string{
"charset": "utf-8",
"protocol": "TCOMPACT",
})
s.app.Post(path, func(c *fiber.Ctx) error {
mBegin := time.Now()
var mDur time.Duration
c.Response().Header.Set("Content-Type", contentTypeResp)
if !bytes.HasPrefix(c.Request().Header.ContentType(), thriftContentType) {
c.Response().Header.Set("x-error-type", "bad-request")
c.Response().Header.Set("x-error-message", "bad request")
return c.SendStatus(400)
}
body := c.Context().Request.Body()
transport := thrift.NewStreamTransport(bytes.NewReader(body), c)
protocol := protocolFactory.GetProtocol(transport)
// put fiber ctx to the context
ctx := WithFiberContext(s.pCtx, c)
// process method
handled, err := processorFactory.GetProcessor(transport).Process(ctx, protocol, protocol)
if err != nil {
s.logger.Errorw("process method", "err", err)
switch err.(type) {
case thrift.TTransportException:
c.Response().Header.Set("x-error-type", "transport-exception")
c.Response().Header.Set("x-error-message", err.Error())
goto internal_error
case thrift.TProtocolException:
c.Response().Header.Set("x-error-type", "protocol-exception")
c.Response().Header.Set("x-error-message", err.Error())
goto internal_error
case thrift.TApplicationException:
c.Response().Header.Set("x-error-type", "application-exception")
c.Response().Header.Set("x-error-message", err.Error())
default:
c.Response().Header.Set("x-error-type", "exception")
c.Response().Header.Set("x-error-message", err.Error())
}
}
mDur = time.Since(mBegin)
c.Response().Header.Set("x-trace-duration", mDur.String())
_ = handled
if err := protocol.Flush(c.Context()); err != nil {
return err
}
return c.SendStatus(200)
internal_error:
// do something!
return c.SendStatus(200)
})
}
func (s *server) run(appCtx *internal.AppContext, addr string) error {
s.pCtx = internal.WithAppContext(context.Background(), appCtx)
s.logger = appCtx.Logger.Sugar().Named("httpsrv")
s.logger.Infow("starting server",
"addr", addr)
return s.app.Listen(addr)
}
func Router() *fiber.App {
return defaultServer.app
}
func GetFiberFromContext(ctx context.Context) *fiber.Ctx {
return ctx.Value(contextKeyFiber).(*fiber.Ctx)
}
func WithFiberContext(ctx context.Context, c *fiber.Ctx) context.Context {
return context.WithValue(ctx, contextKeyFiber, c)
}
func RegisterService(path string, p thrift.TProcessor) {
defaultServer.registerService(path, p)
}
func Run(appCtx *internal.AppContext, addr string) error {
return defaultServer.run(appCtx, addr)
}

61
internal/system/system.go Normal file
View file

@ -0,0 +1,61 @@
package system
import (
"wpw-common/internal"
"go.uber.org/zap"
"gorm.io/gorm"
)
type SystemKV struct {
gorm.Model
Key string
Value string
}
type system struct {
logger *zap.SugaredLogger
db *gorm.DB
}
var Default system
func (s *system) init(appCtx *internal.AppContext) {
logger := appCtx.Logger.Sugar().Named("system")
db, exist := appCtx.Data["db"]
if !exist {
logger.Fatal("system requires `db` in application context")
}
s.logger = logger
s.db = db.(*gorm.DB)
s.db.AutoMigrate(&SystemKV{})
}
func (s *system) DoOnce(key string, fn func() error) (err error) {
var kv SystemKV
kv.Key = key
tx := s.db.First(&kv)
if err = tx.Error; err == gorm.ErrRecordNotFound {
s.logger.Info("doOnce call",
"key", key)
if err = fn(); err != nil {
return
}
tx = s.db.Create(&kv)
if err = tx.Error; err != nil {
return
}
} else if err == nil {
s.logger.Infow("doOnce done",
"key", key,
"at", kv.CreatedAt)
return
}
return
}
func Init(appCtx *internal.AppContext) {
Default.init(appCtx)
}

View file

@ -0,0 +1,31 @@
package vdext
import (
"github.com/go-playground/locales"
"github.com/go-playground/locales/en"
uts "github.com/go-playground/universal-translator"
"github.com/go-playground/validator/v10"
en_translations "github.com/go-playground/validator/v10/translations/en"
)
var (
languages = []locales.Translator{
en.New(),
}
uni = initI8n()
)
func initI8n() *uts.UniversalTranslator {
return uts.New(languages[0], languages[1:]...)
}
func New(tagName string) *validator.Validate {
vd := validator.New()
if trans, found := uni.GetTranslator("en"); found {
en_translations.RegisterDefaultTranslations(vd, trans)
}
vd.SetTagName(tagName)
return vd
}

View file

@ -0,0 +1,330 @@
// Code generated by thriftgo (0.2.1). DO NOT EDIT.
package exceptions
import (
"context"
"fmt"
"github.com/apache/thrift/lib/go/thrift"
"github.com/cloudwego/thriftgo/generator/golang/extension/meta"
"github.com/cloudwego/thriftgo/generator/golang/extension/unknown"
)
type CoreServicesException struct {
Code int32 `thrift:"code,1" frugal:"1,default,i32" db:"code" json:"code"`
Message string `thrift:"message,2" frugal:"2,default,string" db:"message" json:"message"`
Parameters map[string]string `thrift:"parameters,3" frugal:"3,default,map<string:string>" db:"parameters" json:"parameters"`
_unknownFields unknown.Fields
}
func init() {
meta.RegisterStruct(NewCoreServicesException, []byte{
0xb, 0x0, 0x1, 0x0, 0x0, 0x0, 0x15, 0x43,
0x6f, 0x72, 0x65, 0x53, 0x65, 0x72, 0x76, 0x69,
0x63, 0x65, 0x73, 0x45, 0x78, 0x63, 0x65, 0x70,
0x74, 0x69, 0x6f, 0x6e, 0xb, 0x0, 0x2, 0x0,
0x0, 0x0, 0x9, 0x65, 0x78, 0x63, 0x65, 0x70,
0x74, 0x69, 0x6f, 0x6e, 0xf, 0x0, 0x3, 0xc,
0x0, 0x0, 0x0, 0x3, 0x6, 0x0, 0x1, 0x0,
0x1, 0xb, 0x0, 0x2, 0x0, 0x0, 0x0, 0x4,
0x63, 0x6f, 0x64, 0x65, 0x8, 0x0, 0x3, 0x0,
0x0, 0x0, 0x0, 0xc, 0x0, 0x4, 0x8, 0x0,
0x1, 0x0, 0x0, 0x0, 0x8, 0x0, 0x0, 0x6,
0x0, 0x1, 0x0, 0x2, 0xb, 0x0, 0x2, 0x0,
0x0, 0x0, 0x7, 0x6d, 0x65, 0x73, 0x73, 0x61,
0x67, 0x65, 0x8, 0x0, 0x3, 0x0, 0x0, 0x0,
0x0, 0xc, 0x0, 0x4, 0x8, 0x0, 0x1, 0x0,
0x0, 0x0, 0xb, 0x0, 0x0, 0x6, 0x0, 0x1,
0x0, 0x3, 0xb, 0x0, 0x2, 0x0, 0x0, 0x0,
0xa, 0x70, 0x61, 0x72, 0x61, 0x6d, 0x65, 0x74,
0x65, 0x72, 0x73, 0x8, 0x0, 0x3, 0x0, 0x0,
0x0, 0x0, 0xc, 0x0, 0x4, 0x8, 0x0, 0x1,
0x0, 0x0, 0x0, 0xd, 0xc, 0x0, 0x2, 0x8,
0x0, 0x1, 0x0, 0x0, 0x0, 0xb, 0x0, 0xc,
0x0, 0x3, 0x8, 0x0, 0x1, 0x0, 0x0, 0x0,
0xb, 0x0, 0x0, 0x0, 0x0,
})
}
func NewCoreServicesException() *CoreServicesException {
return &CoreServicesException{}
}
func (p *CoreServicesException) InitDefault() {
*p = CoreServicesException{}
}
func (p *CoreServicesException) GetCode() (v int32) {
return p.Code
}
func (p *CoreServicesException) GetMessage() (v string) {
return p.Message
}
func (p *CoreServicesException) GetParameters() (v map[string]string) {
return p.Parameters
}
func (p *CoreServicesException) SetCode(val int32) {
p.Code = val
}
func (p *CoreServicesException) SetMessage(val string) {
p.Message = val
}
func (p *CoreServicesException) SetParameters(val map[string]string) {
p.Parameters = val
}
func (p *CoreServicesException) CarryingUnknownFields() bool {
return len(p._unknownFields) > 0
}
var fieldIDToName_CoreServicesException = map[int16]string{
1: "code",
2: "message",
3: "parameters",
}
func (p *CoreServicesException) Read(ctx context.Context, iprot thrift.TProtocol) (err error) {
var name string
var fieldTypeId thrift.TType
var fieldId int16
if _, err = iprot.ReadStructBegin(ctx); err != nil {
goto ReadStructBeginError
}
for {
name, fieldTypeId, fieldId, err = iprot.ReadFieldBegin(ctx)
if err != nil {
goto ReadFieldBeginError
}
if fieldTypeId == thrift.STOP {
break
}
switch fieldId {
case 1:
if fieldTypeId == thrift.I32 {
if err = p.ReadField1(ctx, iprot); err != nil {
goto ReadFieldError
}
} else {
if err = iprot.Skip(ctx, fieldTypeId); err != nil {
goto SkipFieldError
}
}
case 2:
if fieldTypeId == thrift.STRING {
if err = p.ReadField2(ctx, iprot); err != nil {
goto ReadFieldError
}
} else {
if err = iprot.Skip(ctx, fieldTypeId); err != nil {
goto SkipFieldError
}
}
case 3:
if fieldTypeId == thrift.MAP {
if err = p.ReadField3(ctx, iprot); err != nil {
goto ReadFieldError
}
} else {
if err = iprot.Skip(ctx, fieldTypeId); err != nil {
goto SkipFieldError
}
}
default:
if err = p._unknownFields.Append(ctx, iprot, name, fieldTypeId, fieldId); err != nil {
goto UnknownFieldsAppendError
}
}
if err = iprot.ReadFieldEnd(ctx); err != nil {
goto ReadFieldEndError
}
}
if err = iprot.ReadStructEnd(ctx); err != nil {
goto ReadStructEndError
}
return nil
ReadStructBeginError:
return thrift.PrependError(fmt.Sprintf("%T read struct begin error: ", p), err)
ReadFieldBeginError:
return thrift.PrependError(fmt.Sprintf("%T read field %d begin error: ", p, fieldId), err)
ReadFieldError:
return thrift.PrependError(fmt.Sprintf("%T read field %d '%s' error: ", p, fieldId, fieldIDToName_CoreServicesException[fieldId]), err)
SkipFieldError:
return thrift.PrependError(fmt.Sprintf("%T field %d skip type %d error: ", p, fieldId, fieldTypeId), err)
UnknownFieldsAppendError:
return thrift.PrependError(fmt.Sprintf("%T append unknown field(name:%s type:%d id:%d) error: ", p, name, fieldTypeId, fieldId), err)
ReadFieldEndError:
return thrift.PrependError(fmt.Sprintf("%T read field end error", p), err)
ReadStructEndError:
return thrift.PrependError(fmt.Sprintf("%T read struct end error: ", p), err)
}
func (p *CoreServicesException) ReadField1(ctx context.Context, iprot thrift.TProtocol) error {
if v, err := iprot.ReadI32(ctx); err != nil {
return err
} else {
p.Code = v
}
return nil
}
func (p *CoreServicesException) ReadField2(ctx context.Context, iprot thrift.TProtocol) error {
if v, err := iprot.ReadString(ctx); err != nil {
return err
} else {
p.Message = v
}
return nil
}
func (p *CoreServicesException) ReadField3(ctx context.Context, iprot thrift.TProtocol) error {
_, _, size, err := iprot.ReadMapBegin(ctx)
if err != nil {
return err
}
p.Parameters = make(map[string]string, size)
for i := 0; i < size; i++ {
var _key string
if v, err := iprot.ReadString(ctx); err != nil {
return err
} else {
_key = v
}
var _val string
if v, err := iprot.ReadString(ctx); err != nil {
return err
} else {
_val = v
}
p.Parameters[_key] = _val
}
if err := iprot.ReadMapEnd(ctx); err != nil {
return err
}
return nil
}
func (p *CoreServicesException) Write(ctx context.Context, oprot thrift.TProtocol) (err error) {
var fieldId int16
if err = oprot.WriteStructBegin(ctx, "CoreServicesException"); err != nil {
goto WriteStructBeginError
}
if p != nil {
if err = p.writeField1(ctx, oprot); err != nil {
fieldId = 1
goto WriteFieldError
}
if err = p.writeField2(ctx, oprot); err != nil {
fieldId = 2
goto WriteFieldError
}
if err = p.writeField3(ctx, oprot); err != nil {
fieldId = 3
goto WriteFieldError
}
if err = p._unknownFields.Write(ctx, oprot); err != nil {
goto UnknownFieldsWriteError
}
}
if err = oprot.WriteFieldStop(ctx); err != nil {
goto WriteFieldStopError
}
if err = oprot.WriteStructEnd(ctx); err != nil {
goto WriteStructEndError
}
return nil
WriteStructBeginError:
return thrift.PrependError(fmt.Sprintf("%T write struct begin error: ", p), err)
WriteFieldError:
return thrift.PrependError(fmt.Sprintf("%T write field %d error: ", p, fieldId), err)
WriteFieldStopError:
return thrift.PrependError(fmt.Sprintf("%T write field stop error: ", p), err)
WriteStructEndError:
return thrift.PrependError(fmt.Sprintf("%T write struct end error: ", p), err)
UnknownFieldsWriteError:
return thrift.PrependError(fmt.Sprintf("%T write unknown fields error: ", p), err)
}
func (p *CoreServicesException) writeField1(ctx context.Context, oprot thrift.TProtocol) (err error) {
if err = oprot.WriteFieldBegin(ctx, "code", thrift.I32, 1); err != nil {
goto WriteFieldBeginError
}
if err := oprot.WriteI32(ctx, p.Code); err != nil {
return err
}
if err = oprot.WriteFieldEnd(ctx); err != nil {
goto WriteFieldEndError
}
return nil
WriteFieldBeginError:
return thrift.PrependError(fmt.Sprintf("%T write field 1 begin error: ", p), err)
WriteFieldEndError:
return thrift.PrependError(fmt.Sprintf("%T write field 1 end error: ", p), err)
}
func (p *CoreServicesException) writeField2(ctx context.Context, oprot thrift.TProtocol) (err error) {
if err = oprot.WriteFieldBegin(ctx, "message", thrift.STRING, 2); err != nil {
goto WriteFieldBeginError
}
if err := oprot.WriteString(ctx, p.Message); err != nil {
return err
}
if err = oprot.WriteFieldEnd(ctx); err != nil {
goto WriteFieldEndError
}
return nil
WriteFieldBeginError:
return thrift.PrependError(fmt.Sprintf("%T write field 2 begin error: ", p), err)
WriteFieldEndError:
return thrift.PrependError(fmt.Sprintf("%T write field 2 end error: ", p), err)
}
func (p *CoreServicesException) writeField3(ctx context.Context, oprot thrift.TProtocol) (err error) {
if err = oprot.WriteFieldBegin(ctx, "parameters", thrift.MAP, 3); err != nil {
goto WriteFieldBeginError
}
if err := oprot.WriteMapBegin(ctx, thrift.STRING, thrift.STRING, len(p.Parameters)); err != nil {
return err
}
for k, v := range p.Parameters {
if err := oprot.WriteString(ctx, k); err != nil {
return err
}
if err := oprot.WriteString(ctx, v); err != nil {
return err
}
}
if err := oprot.WriteMapEnd(ctx); err != nil {
return err
}
if err = oprot.WriteFieldEnd(ctx); err != nil {
goto WriteFieldEndError
}
return nil
WriteFieldBeginError:
return thrift.PrependError(fmt.Sprintf("%T write field 3 begin error: ", p), err)
WriteFieldEndError:
return thrift.PrependError(fmt.Sprintf("%T write field 3 end error: ", p), err)
}
func (p *CoreServicesException) String() string {
if p == nil {
return "<nil>"
}
return fmt.Sprintf("CoreServicesException(%+v)", *p)
}
func (p *CoreServicesException) Error() string {
return p.String()
}

4611
pkg/gen/biz/core/service.go Normal file

File diff suppressed because it is too large Load diff

File diff suppressed because it is too large Load diff

View file

@ -0,0 +1,340 @@
// Code generated by thriftgo (0.2.1). DO NOT EDIT.
package exceptionsc
import (
"context"
"fmt"
"github.com/apache/thrift/lib/go/thrift"
"github.com/cloudwego/thriftgo/generator/golang/extension/meta"
"github.com/cloudwego/thriftgo/generator/golang/extension/unknown"
)
type CommonException struct {
Code int32 `thrift:"code,1" frugal:"1,default,i32" db:"code" json:"code"`
Message string `thrift:"message,2" frugal:"2,default,string" db:"message" json:"message"`
Metadata map[string]string `thrift:"metadata,3,optional" frugal:"3,optional,map<string:string>" db:"metadata" json:"metadata,omitempty"`
_unknownFields unknown.Fields
}
func init() {
meta.RegisterStruct(NewCommonException, []byte{
0xb, 0x0, 0x1, 0x0, 0x0, 0x0, 0xf, 0x43,
0x6f, 0x6d, 0x6d, 0x6f, 0x6e, 0x45, 0x78, 0x63,
0x65, 0x70, 0x74, 0x69, 0x6f, 0x6e, 0xb, 0x0,
0x2, 0x0, 0x0, 0x0, 0x9, 0x65, 0x78, 0x63,
0x65, 0x70, 0x74, 0x69, 0x6f, 0x6e, 0xf, 0x0,
0x3, 0xc, 0x0, 0x0, 0x0, 0x3, 0x6, 0x0,
0x1, 0x0, 0x1, 0xb, 0x0, 0x2, 0x0, 0x0,
0x0, 0x4, 0x63, 0x6f, 0x64, 0x65, 0x8, 0x0,
0x3, 0x0, 0x0, 0x0, 0x0, 0xc, 0x0, 0x4,
0x8, 0x0, 0x1, 0x0, 0x0, 0x0, 0x8, 0x0,
0x0, 0x6, 0x0, 0x1, 0x0, 0x2, 0xb, 0x0,
0x2, 0x0, 0x0, 0x0, 0x7, 0x6d, 0x65, 0x73,
0x73, 0x61, 0x67, 0x65, 0x8, 0x0, 0x3, 0x0,
0x0, 0x0, 0x0, 0xc, 0x0, 0x4, 0x8, 0x0,
0x1, 0x0, 0x0, 0x0, 0xb, 0x0, 0x0, 0x6,
0x0, 0x1, 0x0, 0x3, 0xb, 0x0, 0x2, 0x0,
0x0, 0x0, 0x8, 0x6d, 0x65, 0x74, 0x61, 0x64,
0x61, 0x74, 0x61, 0x8, 0x0, 0x3, 0x0, 0x0,
0x0, 0x2, 0xc, 0x0, 0x4, 0x8, 0x0, 0x1,
0x0, 0x0, 0x0, 0xd, 0xc, 0x0, 0x2, 0x8,
0x0, 0x1, 0x0, 0x0, 0x0, 0xb, 0x0, 0xc,
0x0, 0x3, 0x8, 0x0, 0x1, 0x0, 0x0, 0x0,
0xb, 0x0, 0x0, 0x0, 0x0,
})
}
func NewCommonException() *CommonException {
return &CommonException{}
}
func (p *CommonException) InitDefault() {
*p = CommonException{}
}
func (p *CommonException) GetCode() (v int32) {
return p.Code
}
func (p *CommonException) GetMessage() (v string) {
return p.Message
}
var CommonException_Metadata_DEFAULT map[string]string
func (p *CommonException) GetMetadata() (v map[string]string) {
if !p.IsSetMetadata() {
return CommonException_Metadata_DEFAULT
}
return p.Metadata
}
func (p *CommonException) SetCode(val int32) {
p.Code = val
}
func (p *CommonException) SetMessage(val string) {
p.Message = val
}
func (p *CommonException) SetMetadata(val map[string]string) {
p.Metadata = val
}
func (p *CommonException) CarryingUnknownFields() bool {
return len(p._unknownFields) > 0
}
var fieldIDToName_CommonException = map[int16]string{
1: "code",
2: "message",
3: "metadata",
}
func (p *CommonException) IsSetMetadata() bool {
return p.Metadata != nil
}
func (p *CommonException) Read(ctx context.Context, iprot thrift.TProtocol) (err error) {
var name string
var fieldTypeId thrift.TType
var fieldId int16
if _, err = iprot.ReadStructBegin(ctx); err != nil {
goto ReadStructBeginError
}
for {
name, fieldTypeId, fieldId, err = iprot.ReadFieldBegin(ctx)
if err != nil {
goto ReadFieldBeginError
}
if fieldTypeId == thrift.STOP {
break
}
switch fieldId {
case 1:
if fieldTypeId == thrift.I32 {
if err = p.ReadField1(ctx, iprot); err != nil {
goto ReadFieldError
}
} else {
if err = iprot.Skip(ctx, fieldTypeId); err != nil {
goto SkipFieldError
}
}
case 2:
if fieldTypeId == thrift.STRING {
if err = p.ReadField2(ctx, iprot); err != nil {
goto ReadFieldError
}
} else {
if err = iprot.Skip(ctx, fieldTypeId); err != nil {
goto SkipFieldError
}
}
case 3:
if fieldTypeId == thrift.MAP {
if err = p.ReadField3(ctx, iprot); err != nil {
goto ReadFieldError
}
} else {
if err = iprot.Skip(ctx, fieldTypeId); err != nil {
goto SkipFieldError
}
}
default:
if err = p._unknownFields.Append(ctx, iprot, name, fieldTypeId, fieldId); err != nil {
goto UnknownFieldsAppendError
}
}
if err = iprot.ReadFieldEnd(ctx); err != nil {
goto ReadFieldEndError
}
}
if err = iprot.ReadStructEnd(ctx); err != nil {
goto ReadStructEndError
}
return nil
ReadStructBeginError:
return thrift.PrependError(fmt.Sprintf("%T read struct begin error: ", p), err)
ReadFieldBeginError:
return thrift.PrependError(fmt.Sprintf("%T read field %d begin error: ", p, fieldId), err)
ReadFieldError:
return thrift.PrependError(fmt.Sprintf("%T read field %d '%s' error: ", p, fieldId, fieldIDToName_CommonException[fieldId]), err)
SkipFieldError:
return thrift.PrependError(fmt.Sprintf("%T field %d skip type %d error: ", p, fieldId, fieldTypeId), err)
UnknownFieldsAppendError:
return thrift.PrependError(fmt.Sprintf("%T append unknown field(name:%s type:%d id:%d) error: ", p, name, fieldTypeId, fieldId), err)
ReadFieldEndError:
return thrift.PrependError(fmt.Sprintf("%T read field end error", p), err)
ReadStructEndError:
return thrift.PrependError(fmt.Sprintf("%T read struct end error: ", p), err)
}
func (p *CommonException) ReadField1(ctx context.Context, iprot thrift.TProtocol) error {
if v, err := iprot.ReadI32(ctx); err != nil {
return err
} else {
p.Code = v
}
return nil
}
func (p *CommonException) ReadField2(ctx context.Context, iprot thrift.TProtocol) error {
if v, err := iprot.ReadString(ctx); err != nil {
return err
} else {
p.Message = v
}
return nil
}
func (p *CommonException) ReadField3(ctx context.Context, iprot thrift.TProtocol) error {
_, _, size, err := iprot.ReadMapBegin(ctx)
if err != nil {
return err
}
p.Metadata = make(map[string]string, size)
for i := 0; i < size; i++ {
var _key string
if v, err := iprot.ReadString(ctx); err != nil {
return err
} else {
_key = v
}
var _val string
if v, err := iprot.ReadString(ctx); err != nil {
return err
} else {
_val = v
}
p.Metadata[_key] = _val
}
if err := iprot.ReadMapEnd(ctx); err != nil {
return err
}
return nil
}
func (p *CommonException) Write(ctx context.Context, oprot thrift.TProtocol) (err error) {
var fieldId int16
if err = oprot.WriteStructBegin(ctx, "CommonException"); err != nil {
goto WriteStructBeginError
}
if p != nil {
if err = p.writeField1(ctx, oprot); err != nil {
fieldId = 1
goto WriteFieldError
}
if err = p.writeField2(ctx, oprot); err != nil {
fieldId = 2
goto WriteFieldError
}
if err = p.writeField3(ctx, oprot); err != nil {
fieldId = 3
goto WriteFieldError
}
if err = p._unknownFields.Write(ctx, oprot); err != nil {
goto UnknownFieldsWriteError
}
}
if err = oprot.WriteFieldStop(ctx); err != nil {
goto WriteFieldStopError
}
if err = oprot.WriteStructEnd(ctx); err != nil {
goto WriteStructEndError
}
return nil
WriteStructBeginError:
return thrift.PrependError(fmt.Sprintf("%T write struct begin error: ", p), err)
WriteFieldError:
return thrift.PrependError(fmt.Sprintf("%T write field %d error: ", p, fieldId), err)
WriteFieldStopError:
return thrift.PrependError(fmt.Sprintf("%T write field stop error: ", p), err)
WriteStructEndError:
return thrift.PrependError(fmt.Sprintf("%T write struct end error: ", p), err)
UnknownFieldsWriteError:
return thrift.PrependError(fmt.Sprintf("%T write unknown fields error: ", p), err)
}
func (p *CommonException) writeField1(ctx context.Context, oprot thrift.TProtocol) (err error) {
if err = oprot.WriteFieldBegin(ctx, "code", thrift.I32, 1); err != nil {
goto WriteFieldBeginError
}
if err := oprot.WriteI32(ctx, p.Code); err != nil {
return err
}
if err = oprot.WriteFieldEnd(ctx); err != nil {
goto WriteFieldEndError
}
return nil
WriteFieldBeginError:
return thrift.PrependError(fmt.Sprintf("%T write field 1 begin error: ", p), err)
WriteFieldEndError:
return thrift.PrependError(fmt.Sprintf("%T write field 1 end error: ", p), err)
}
func (p *CommonException) writeField2(ctx context.Context, oprot thrift.TProtocol) (err error) {
if err = oprot.WriteFieldBegin(ctx, "message", thrift.STRING, 2); err != nil {
goto WriteFieldBeginError
}
if err := oprot.WriteString(ctx, p.Message); err != nil {
return err
}
if err = oprot.WriteFieldEnd(ctx); err != nil {
goto WriteFieldEndError
}
return nil
WriteFieldBeginError:
return thrift.PrependError(fmt.Sprintf("%T write field 2 begin error: ", p), err)
WriteFieldEndError:
return thrift.PrependError(fmt.Sprintf("%T write field 2 end error: ", p), err)
}
func (p *CommonException) writeField3(ctx context.Context, oprot thrift.TProtocol) (err error) {
if p.IsSetMetadata() {
if err = oprot.WriteFieldBegin(ctx, "metadata", thrift.MAP, 3); err != nil {
goto WriteFieldBeginError
}
if err := oprot.WriteMapBegin(ctx, thrift.STRING, thrift.STRING, len(p.Metadata)); err != nil {
return err
}
for k, v := range p.Metadata {
if err := oprot.WriteString(ctx, k); err != nil {
return err
}
if err := oprot.WriteString(ctx, v); err != nil {
return err
}
}
if err := oprot.WriteMapEnd(ctx); err != nil {
return err
}
if err = oprot.WriteFieldEnd(ctx); err != nil {
goto WriteFieldEndError
}
}
return nil
WriteFieldBeginError:
return thrift.PrependError(fmt.Sprintf("%T write field 3 begin error: ", p), err)
WriteFieldEndError:
return thrift.PrependError(fmt.Sprintf("%T write field 3 end error: ", p), err)
}
func (p *CommonException) String() string {
if p == nil {
return "<nil>"
}
return fmt.Sprintf("CommonException(%+v)", *p)
}
func (p *CommonException) Error() string {
return p.String()
}

View file

@ -0,0 +1,5 @@
// Code generated by thriftgo (0.2.1). DO NOT EDIT.
package common
import ()

90
pkg/gormlog/gormlog.go Normal file
View file

@ -0,0 +1,90 @@
package gormlog
import (
"context"
"errors"
"fmt"
"time"
"go.uber.org/zap"
"gorm.io/gorm/logger"
"gorm.io/gorm/utils"
)
type GormLog struct {
GormLogParams
sug *zap.SugaredLogger
}
var _ logger.Interface = (*GormLog)(nil)
type GormLogParams struct {
LogLevel logger.LogLevel
SlowThreshold time.Duration
IgnoreRecordNotFoundError bool
Logger *zap.Logger
}
func New(params GormLogParams) *GormLog {
return &GormLog{
GormLogParams: params,
sug: params.Logger.Sugar().Named("gormlog"),
}
}
func (l *GormLog) LogMode(level logger.LogLevel) logger.Interface {
var params = l.GormLogParams
params.LogLevel = level
return New(params)
}
func (l *GormLog) Info(ctx context.Context, msg string, data ...any) {
if l.LogLevel >= logger.Info {
l.sug.Info(append([]any{msg}, data...)...)
}
}
func (l *GormLog) Warn(ctx context.Context, msg string, data ...any) {
if l.LogLevel >= logger.Warn {
l.sug.Warn(append([]any{msg}, data...)...)
}
}
func (l *GormLog) Error(ctx context.Context, msg string, data ...any) {
if l.LogLevel >= logger.Error {
l.sug.Error(append([]any{msg}, data...)...)
}
}
func (l *GormLog) Trace(ctx context.Context, begin time.Time, fc func() (sql string, rowsAffected int64), err error) {
if l.LogLevel <= logger.Silent {
return
}
elapsed := time.Since(begin)
switch {
case err != nil && l.LogLevel >= logger.Error && (!errors.Is(err, logger.ErrRecordNotFound) || !l.IgnoreRecordNotFoundError):
sql, rows := fc()
if rows != -1 {
l.sug.Errorf("trace %v %v %v %s %s", utils.FileWithLineNum(), err, elapsed.Milliseconds(), "-", sql)
} else {
l.sug.Errorf("trace %v %v %v %s %s", utils.FileWithLineNum(), err, elapsed.Milliseconds(), rows, sql)
}
case elapsed > l.SlowThreshold && l.SlowThreshold != 0 && l.LogLevel >= logger.Warn:
sql, rows := fc()
slowLog := fmt.Sprintf("SLOW SQL >= %v", l.SlowThreshold)
if rows == -1 {
l.sug.Warnf("trace %v %v %v %s %s", utils.FileWithLineNum(), slowLog, elapsed.Milliseconds(), "-", sql)
} else {
l.sug.Warnf("trace %v %v %v %s %s", utils.FileWithLineNum(), slowLog, elapsed.Milliseconds(), rows, sql)
}
case l.LogLevel == logger.Info:
sql, rows := fc()
if rows != -1 {
l.sug.Infof("trace %v %v %s %s", utils.FileWithLineNum(), elapsed.Milliseconds(), "-", sql)
} else {
l.sug.Infof("trace %v %v %s %s", utils.FileWithLineNum(), elapsed.Milliseconds(), rows, sql)
}
}
}

File diff suppressed because it is too large Load diff

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

15
public/index.html Normal file
View file

@ -0,0 +1,15 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<link rel="icon" type="image/svg+xml" href="/vite.svg" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>Vite + Vue + TS</title>
<script type="module" crossorigin src="/assets/index.241d8ed5.js"></script>
<link rel="stylesheet" href="/assets/index.d6f403e9.css">
</head>
<body>
<div id="app"></div>
</body>
</html>

14
public/manifest.json Normal file
View file

@ -0,0 +1,14 @@
{
"index.html": {
"file": "assets/index.241d8ed5.js",
"src": "index.html",
"isEntry": true,
"css": [
"assets/index.d6f403e9.css"
]
},
"index.css": {
"file": "assets/index.d6f403e9.css",
"src": "index.css"
}
}

6
public/public.go Normal file
View file

@ -0,0 +1,6 @@
package public
import "embed"
//go:embed *
var FS embed.FS

1
public/vite.svg Normal file
View file

@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" aria-hidden="true" role="img" class="iconify iconify--logos" width="31.88" height="32" preserveAspectRatio="xMidYMid meet" viewBox="0 0 256 257"><defs><linearGradient id="IconifyId1813088fe1fbc01fb466" x1="-.828%" x2="57.636%" y1="7.652%" y2="78.411%"><stop offset="0%" stop-color="#41D1FF"></stop><stop offset="100%" stop-color="#BD34FE"></stop></linearGradient><linearGradient id="IconifyId1813088fe1fbc01fb467" x1="43.376%" x2="50.316%" y1="2.242%" y2="89.03%"><stop offset="0%" stop-color="#FFEA83"></stop><stop offset="8.333%" stop-color="#FFDD35"></stop><stop offset="100%" stop-color="#FFA800"></stop></linearGradient></defs><path fill="url(#IconifyId1813088fe1fbc01fb466)" d="M255.153 37.938L134.897 252.976c-2.483 4.44-8.862 4.466-11.382.048L.875 37.958c-2.746-4.814 1.371-10.646 6.827-9.67l120.385 21.517a6.537 6.537 0 0 0 2.322-.004l117.867-21.483c5.438-.991 9.574 4.796 6.877 9.62Z"></path><path fill="url(#IconifyId1813088fe1fbc01fb467)" d="M185.432.063L96.44 17.501a3.268 3.268 0 0 0-2.634 3.014l-5.474 92.456a3.268 3.268 0 0 0 3.997 3.378l24.777-5.718c2.318-.535 4.413 1.507 3.936 3.838l-7.361 36.047c-.495 2.426 1.782 4.5 4.151 3.78l15.304-4.649c2.372-.72 4.652 1.36 4.15 3.788l-11.698 56.621c-.732 3.542 3.979 5.473 5.943 2.437l1.313-2.028l72.516-144.72c1.215-2.423-.88-5.186-3.54-4.672l-25.505 4.922c-2.396.462-4.435-1.77-3.759-4.114l16.646-57.705c.677-2.35-1.37-4.583-3.769-4.113Z"></path></svg>

After

Width:  |  Height:  |  Size: 1.5 KiB

8
tools.go Normal file
View file

@ -0,0 +1,8 @@
//go:build tools
// +build tools
package wpw
import (
_ "github.com/ii64/thrift-idl-builder"
)