mirror of
https://github.com/muety/wakapi.git
synced 2023-08-10 21:12:56 +03:00
Compare commits
26 Commits
Author | SHA1 | Date | |
---|---|---|---|
b2a3579be9 | |||
42a6e9d923 | |||
1f44ccadba | |||
6ea72c6d02 | |||
d93348842a | |||
fb92747129 | |||
4e6e665e19 | |||
a3d8c4d464 | |||
e9eaa9da53 | |||
5adb795f59 | |||
a552073d18 | |||
de0401d4bb | |||
c39538db13 | |||
189a09d91f | |||
d57c02af7c | |||
16b683fcbd | |||
acda62488d | |||
1aecfc4ca3 | |||
cd97976ed5 | |||
3a4504d56a | |||
a018f70c3f | |||
a03e49e7f0 | |||
ec81d9fe5d | |||
b7a1e2d795 | |||
98b62b33c8 | |||
262bee9022 |
@ -1 +1,10 @@
|
|||||||
.env
|
.env
|
||||||
|
config*.yml
|
||||||
|
!config.default.yml
|
||||||
|
*.db
|
||||||
|
*.exe
|
||||||
|
wakapi
|
||||||
|
Dockerfile
|
||||||
|
docker-compose.yml
|
||||||
|
.dockerignore
|
||||||
|
.git*
|
||||||
|
19
.github/workflows/linux-build-on-release.yml
vendored
19
.github/workflows/linux-build-on-release.yml
vendored
@ -1,13 +1,15 @@
|
|||||||
name: Build Wakapi on Linux
|
name: Build Wakapi on Linux
|
||||||
|
|
||||||
on:
|
on:
|
||||||
|
push:
|
||||||
|
pull_request:
|
||||||
release:
|
release:
|
||||||
types:
|
types:
|
||||||
- created
|
- created
|
||||||
|
|
||||||
jobs:
|
jobs:
|
||||||
build-and-release:
|
build-and-release:
|
||||||
name: Build and add to Release
|
name: Build
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
steps:
|
steps:
|
||||||
|
|
||||||
@ -22,18 +24,21 @@ jobs:
|
|||||||
|
|
||||||
- name: Get dependencies
|
- name: Get dependencies
|
||||||
run: |
|
run: |
|
||||||
go get -v -t -d ./...
|
go get github.com/markbates/pkger/cmd/pkger
|
||||||
|
go get
|
||||||
|
go generate
|
||||||
|
|
||||||
- name: Build
|
- name: Build
|
||||||
run: GO111MODULE=on go build -v .
|
run: GO111MODULE=on go build -v .
|
||||||
|
|
||||||
- name: Zip Release
|
- name: Zip executable and sample config
|
||||||
uses: TheDoctor0/zip-release@v0.3.0
|
if: github.event_name == 'release'
|
||||||
with:
|
run: |
|
||||||
filename: release.zip
|
cp config.default.yml config.yml
|
||||||
exclusions: '*.git*'
|
zip -9 release.zip wakapi config.yml
|
||||||
|
|
||||||
- name: Upload built executable to Release
|
- name: Upload built executable to Release
|
||||||
|
if: github.event_name == 'release'
|
||||||
uses: actions/upload-release-asset@v1.0.2
|
uses: actions/upload-release-asset@v1.0.2
|
||||||
env:
|
env:
|
||||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||||
|
14
.github/workflows/win-build-on-release.yml
vendored
14
.github/workflows/win-build-on-release.yml
vendored
@ -1,13 +1,15 @@
|
|||||||
name: Build Wakapi on Windows
|
name: Build Wakapi on Windows
|
||||||
|
|
||||||
on:
|
on:
|
||||||
|
push:
|
||||||
|
pull_request:
|
||||||
release:
|
release:
|
||||||
types:
|
types:
|
||||||
- created
|
- created
|
||||||
|
|
||||||
jobs:
|
jobs:
|
||||||
build-and-release:
|
build-and-release:
|
||||||
name: Build and add to release
|
name: Build
|
||||||
runs-on: windows-latest
|
runs-on: windows-latest
|
||||||
steps:
|
steps:
|
||||||
|
|
||||||
@ -22,7 +24,9 @@ jobs:
|
|||||||
|
|
||||||
- name: Get dependencies
|
- name: Get dependencies
|
||||||
run: |
|
run: |
|
||||||
go get -v -t -d ./...
|
go get github.com/markbates/pkger/cmd/pkger
|
||||||
|
go get
|
||||||
|
go generate
|
||||||
|
|
||||||
- name: Enable Go 1.11 modules
|
- name: Enable Go 1.11 modules
|
||||||
run: cmd /c "set GO111MODULE=on"
|
run: cmd /c "set GO111MODULE=on"
|
||||||
@ -31,9 +35,13 @@ jobs:
|
|||||||
run: go build -v .
|
run: go build -v .
|
||||||
|
|
||||||
- name: Compress working folder
|
- name: Compress working folder
|
||||||
run: Compress-Archive -Path .\* -DestinationPath release.zip
|
if: github.event_name == 'release'
|
||||||
|
run: |
|
||||||
|
cp .\config.default.yml .\config.yml
|
||||||
|
Compress-Archive -Path .\wakapi.exe, .\config.yml -DestinationPath release.zip
|
||||||
|
|
||||||
- name: Upload built executable to Release
|
- name: Upload built executable to Release
|
||||||
|
if: github.event_name == 'release'
|
||||||
uses: actions/upload-release-asset@v1.0.2
|
uses: actions/upload-release-asset@v1.0.2
|
||||||
env:
|
env:
|
||||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||||
|
1
.gitignore
vendored
1
.gitignore
vendored
@ -9,3 +9,4 @@ build
|
|||||||
config*.yml
|
config*.yml
|
||||||
!config.default.yml
|
!config.default.yml
|
||||||
config.ini
|
config.ini
|
||||||
|
pkged.go
|
||||||
|
40
Dockerfile
40
Dockerfile
@ -1,26 +1,25 @@
|
|||||||
# Build Stage
|
# Build Stage
|
||||||
|
|
||||||
FROM golang:1.13 AS build-env
|
FROM golang:1.15 AS build-env
|
||||||
WORKDIR /src
|
WORKDIR /src
|
||||||
|
|
||||||
ADD ./go.mod .
|
ADD ./go.mod .
|
||||||
RUN go mod download
|
RUN go mod download && go get github.com/markbates/pkger/cmd/pkger
|
||||||
|
|
||||||
ADD . .
|
ADD . .
|
||||||
RUN go build -o wakapi
|
RUN go generate && go build -o wakapi
|
||||||
|
|
||||||
# Final Stage
|
WORKDIR /app
|
||||||
|
RUN cp /src/wakapi . && \
|
||||||
|
cp /src/config.default.yml config.yml && \
|
||||||
|
sed -i 's/listen_ipv6: ::1/listen_ipv6: /g' config.yml && \
|
||||||
|
cp /src/wait-for-it.sh .
|
||||||
|
|
||||||
|
# Run Stage
|
||||||
|
|
||||||
# When running the application using `docker run`, you can pass environment variables
|
# When running the application using `docker run`, you can pass environment variables
|
||||||
# to override config values using `-e` syntax.
|
# to override config values using `-e` syntax.
|
||||||
# Available options are:
|
# Available options can be found in [README.md#-configuration](README.md#-configuration)
|
||||||
# – WAKAPI_DB_TYPE
|
|
||||||
# – WAKAPI_DB_USER
|
|
||||||
# – WAKAPI_DB_PASSWORD
|
|
||||||
# – WAKAPI_DB_HOST
|
|
||||||
# – WAKAPI_DB_PORT
|
|
||||||
# – WAKAPI_DB_NAME
|
|
||||||
# – WAKAPI_PASSWORD_SALT
|
|
||||||
# – WAKAPI_BASE_PATH
|
|
||||||
|
|
||||||
FROM debian
|
FROM debian
|
||||||
WORKDIR /app
|
WORKDIR /app
|
||||||
@ -32,19 +31,10 @@ ENV WAKAPI_DB_PASSWORD ''
|
|||||||
ENV WAKAPI_DB_HOST ''
|
ENV WAKAPI_DB_HOST ''
|
||||||
ENV WAKAPI_DB_NAME=/data/wakapi.db
|
ENV WAKAPI_DB_NAME=/data/wakapi.db
|
||||||
ENV WAKAPI_PASSWORD_SALT ''
|
ENV WAKAPI_PASSWORD_SALT ''
|
||||||
|
ENV WAKAPI_LISTEN_IPV4 '0.0.0.0'
|
||||||
|
ENV WAKAPI_INSECURE_COOKIES 'true'
|
||||||
|
|
||||||
COPY --from=build-env /src/wakapi /app/
|
COPY --from=build-env /app .
|
||||||
COPY --from=build-env /src/config.default.yml /app/config.yml
|
|
||||||
COPY --from=build-env /src/version.txt /app/
|
|
||||||
|
|
||||||
RUN sed -i 's/listen_ipv4: 127.0.0.1/listen_ipv4: 0.0.0.0/g' /app/config.yml
|
|
||||||
RUN sed -i 's/insecure_cookies: false/insecure_cookies: true/g' /app/config.yml
|
|
||||||
|
|
||||||
ADD static /app/static
|
|
||||||
ADD data /app/data
|
|
||||||
ADD migrations /app/migrations
|
|
||||||
ADD views /app/views
|
|
||||||
ADD wait-for-it.sh .
|
|
||||||
|
|
||||||
VOLUME /data
|
VOLUME /data
|
||||||
|
|
||||||
|
41
README.md
41
README.md
@ -36,16 +36,16 @@ To use the hosted version set `api_url = https://wakapi.dev/api/heartbeat`. Howe
|
|||||||
* Fedora / RHEL: `dnf install @development-tools`
|
* Fedora / RHEL: `dnf install @development-tools`
|
||||||
* Ubuntu / Debian: `apt install build-essential`
|
* Ubuntu / Debian: `apt install build-essential`
|
||||||
* Windows: See [here](https://github.com/mattn/go-sqlite3/issues/214#issuecomment-253216476)
|
* Windows: See [here](https://github.com/mattn/go-sqlite3/issues/214#issuecomment-253216476)
|
||||||
* _Optional_: A MySQL- or Postgres database
|
* _Optional_: One of the [supported databases](#supported-databases)
|
||||||
|
|
||||||
**On your local machine:**
|
**On your local machine:**
|
||||||
* [WakaTime plugin](https://wakatime.com/plugins) for your editor / IDE
|
* [WakaTime plugin](https://wakatime.com/plugins) for your editor / IDE
|
||||||
|
|
||||||
## ⌨️ Server Setup
|
## ⌨️ Server Setup
|
||||||
### Run from source
|
### Run from source
|
||||||
1. Clone the project
|
|
||||||
1. Copy `config.default.yml` to `config.yml` and adapt it to your needs
|
1. Copy `config.default.yml` to `config.yml` and adapt it to your needs
|
||||||
1. Build executable: `GO111MODULE=on go build`
|
1. Install packaging tool: `GO111MODULE=on go get github.com/markbates/pkger/cmd/pkger`
|
||||||
|
1. Build executable: `GO111MODULE=on go generate && go build -o wakapi`
|
||||||
1. Run server: `./wakapi`
|
1. Run server: `./wakapi`
|
||||||
|
|
||||||
**As an alternative** to building from source you can also grab a pre-built [release](https://github.com/muety/wakapi/releases). Steps 2, 3 and 5 apply analogously.
|
**As an alternative** to building from source you can also grab a pre-built [release](https://github.com/muety/wakapi/releases). Steps 2, 3 and 5 apply analogously.
|
||||||
@ -54,7 +54,7 @@ To use the hosted version set `api_url = https://wakapi.dev/api/heartbeat`. Howe
|
|||||||
|
|
||||||
### Run with Docker
|
### Run with Docker
|
||||||
```bash
|
```bash
|
||||||
docker run -d -p 3000:3000 --name wakapi n1try/wakapi
|
docker run -d -p 3000:3000 -e "WAKAPI_PASSWORD_SALT=$(cat /dev/urandom | tr -dc 'a-zA-Z0-9' | fold -w ${1:-32} | head -n 1)" --name wakapi n1try/wakapi
|
||||||
```
|
```
|
||||||
|
|
||||||
By default, SQLite is used as a database. To run Wakapi in Docker with MySQL or Postgres, see [Dockerfile](https://github.com/muety/wakapi/blob/master/Dockerfile) and [config.default.yml](https://github.com/muety/wakapi/blob/master/config.default.yml) for further options.
|
By default, SQLite is used as a database. To run Wakapi in Docker with MySQL or Postgres, see [Dockerfile](https://github.com/muety/wakapi/blob/master/Dockerfile) and [config.default.yml](https://github.com/muety/wakapi/blob/master/config.default.yml) for further options.
|
||||||
@ -73,7 +73,7 @@ You can specify configuration options either via a config file (default: `config
|
|||||||
| `app.custom_languages` | - | - | Map from file endings to language names |
|
| `app.custom_languages` | - | - | Map from file endings to language names |
|
||||||
| `server.port` | `WAKAPI_PORT` | `3000` | Port to listen on |
|
| `server.port` | `WAKAPI_PORT` | `3000` | Port to listen on |
|
||||||
| `server.listen_ipv4` | `WAKAPI_LISTEN_IPV4` | `127.0.0.1` | IPv4 network address to listen on (leave blank to disable IPv4) |
|
| `server.listen_ipv4` | `WAKAPI_LISTEN_IPV4` | `127.0.0.1` | IPv4 network address to listen on (leave blank to disable IPv4) |
|
||||||
| `server.listen_ipv6` | `WAKAPI_LISTEN_IPV6` | `` | IPv6 network address to listen on (leave blank to disable IPv6) |
|
| `server.listen_ipv6` | `WAKAPI_LISTEN_IPV6` | `::1` | IPv6 network address to listen on (leave blank to disable IPv6) |
|
||||||
| `server.tls_cert_path` | `WAKAPI_TLS_CERT_PATH` | - | Path of SSL server certificate (leave blank to not use HTTPS) |
|
| `server.tls_cert_path` | `WAKAPI_TLS_CERT_PATH` | - | Path of SSL server certificate (leave blank to not use HTTPS) |
|
||||||
| `server.tls_key_path` | `WAKAPI_TLS_KEY_PATH` | - | Path of SSL server private key (leave blank to not use HTTPS) |
|
| `server.tls_key_path` | `WAKAPI_TLS_KEY_PATH` | - | Path of SSL server private key (leave blank to not use HTTPS) |
|
||||||
| `server.base_path` | `WAKAPI_BASE_PATH` | `/` | Web base path (change when running behind a proxy under a sub-path) |
|
| `server.base_path` | `WAKAPI_BASE_PATH` | `/` | Web base path (change when running behind a proxy under a sub-path) |
|
||||||
@ -85,8 +85,17 @@ You can specify configuration options either via a config file (default: `config
|
|||||||
| `db.user` | `WAKAPI_DB_USER` | - | Database user |
|
| `db.user` | `WAKAPI_DB_USER` | - | Database user |
|
||||||
| `db.password` | `WAKAPI_DB_PASSWORD` | - | Database password |
|
| `db.password` | `WAKAPI_DB_PASSWORD` | - | Database password |
|
||||||
| `db.name` | `WAKAPI_DB_NAME` | `wakapi_db.db` | Database name |
|
| `db.name` | `WAKAPI_DB_NAME` | `wakapi_db.db` | Database name |
|
||||||
| `db.dialect` | `WAKAPI_DB_TYPE` | `sqlite3` | Database type (one of sqlite3, mysql, postgres) |
|
| `db.dialect` | `WAKAPI_DB_TYPE` | `sqlite3` | Database type (one of `sqlite3`, `mysql`, `postgres`, `cockroach`) |
|
||||||
| `db.max_conn` | `WAKAPI_DB_MAX_CONNECTIONS` | `2` | Maximum number of database connections |
|
| `db.max_conn` | `WAKAPI_DB_MAX_CONNECTIONS` | `2` | Maximum number of database connections |
|
||||||
|
| `db.ssl` | `WAKAPI_DB_SSL` | `false` | Whether to use TLS encryption for database connection (Postgres and CockroachDB only) |
|
||||||
|
|
||||||
|
### Supported databases
|
||||||
|
Wakapi uses [GORM](https://gorm.io) as an ORM. As a consequence, a set of different relational databases is supported.
|
||||||
|
* [SQLite](https://sqlite.org/) (_default, easy setup_)
|
||||||
|
* [MySQL](https://hub.docker.com/_/mysql) (_recommended, because most extensively tested_)
|
||||||
|
* [MariaDB](https://hub.docker.com/_/mariadb) (_open-source MySQL alternative_)
|
||||||
|
* [Postgres](https://hub.docker.com/_/postgres) (_open-source as well_)
|
||||||
|
* [CockroachDB](https://www.cockroachlabs.com/docs/stable/install-cockroachdb-linux.html) (_cloud-native, distributed, Postgres-compatible API_)
|
||||||
|
|
||||||
## 💻 Client Setup
|
## 💻 Client Setup
|
||||||
Wakapi relies on the open-source [WakaTime](https://github.com/wakatime/wakatime) client tools. In order to collect statistics to Wakapi, you need to set them up.
|
Wakapi relies on the open-source [WakaTime](https://github.com/wakatime/wakatime) client tools. In order to collect statistics to Wakapi, you need to set them up.
|
||||||
@ -102,26 +111,10 @@ api_key = the_api_key_printed_to_the_console_after_starting_the_server`
|
|||||||
You can view your API Key after logging in to the web interface.
|
You can view your API Key after logging in to the web interface.
|
||||||
|
|
||||||
### Optional: Client-side proxy
|
### Optional: Client-side proxy
|
||||||
|
|
||||||
See the [advanced setup instructions](docs/advanced_setup.md).
|
See the [advanced setup instructions](docs/advanced_setup.md).
|
||||||
|
|
||||||
## 🔵 Customization
|
### Optional: WakaTime relay
|
||||||
|
You can connect Wakapi with WakaTime in a way that all heartbeats sent to Wakapi are relayed. This way, you can use both services at the same time. Go to the settings page of your instance to configure this integration.
|
||||||
### Aliases
|
|
||||||
There is an option to add aliases for project names, editors, operating systems and languages. For instance, if you want to map two projects – `myapp-frontend` and `myapp-backend` – two a common project name – `myapp-web` – in your statistics, you can add project aliases.
|
|
||||||
|
|
||||||
At the moment, this can only be done via raw database queries. For the above example, you would need to add two aliases, like this:
|
|
||||||
|
|
||||||
```sql
|
|
||||||
INSERT INTO aliases (`type`, `user_id`, `key`, `value`) VALUES (0, 'your_username', 'myapp-web', 'myapp-frontend');
|
|
||||||
```
|
|
||||||
|
|
||||||
#### Types
|
|
||||||
* Project ~ type **0**
|
|
||||||
* Language ~ type **1**
|
|
||||||
* Editor ~ type **2**
|
|
||||||
* OS ~ type **3**
|
|
||||||
* Machine ~ type **4**
|
|
||||||
|
|
||||||
## 🔧 API Endpoints
|
## 🔧 API Endpoints
|
||||||
The following API endpoints are available. A more detailed Swagger documentation is about to come ([#40](https://github.com/muety/wakapi/issues/40)).
|
The following API endpoints are available. A more detailed Swagger documentation is about to come ([#40](https://github.com/muety/wakapi/issues/40)).
|
||||||
|
@ -10,6 +10,7 @@ server:
|
|||||||
|
|
||||||
app:
|
app:
|
||||||
aggregation_time: '02:15' # time at which to run daily aggregation batch jobs
|
aggregation_time: '02:15' # time at which to run daily aggregation batch jobs
|
||||||
|
counting_time: '05:15' # time at which to run daily job to count total hours tracked in the system
|
||||||
custom_languages:
|
custom_languages:
|
||||||
vue: Vue
|
vue: Vue
|
||||||
jsx: JSX
|
jsx: JSX
|
||||||
@ -21,7 +22,8 @@ db:
|
|||||||
password: # leave blank when using sqlite3
|
password: # leave blank when using sqlite3
|
||||||
name: wakapi_db.db # database name for mysql / postgres or file path for sqlite (e.g. /tmp/wakapi.db)
|
name: wakapi_db.db # database name for mysql / postgres or file path for sqlite (e.g. /tmp/wakapi.db)
|
||||||
dialect: sqlite3 # mysql, postgres, sqlite3
|
dialect: sqlite3 # mysql, postgres, sqlite3
|
||||||
max_conn: 2
|
max_conn: 2 # maximum number of concurrent connections to maintain
|
||||||
|
ssl: false # whether to use tls for db connection (must be true for cockroachdb) (ignored for mysql and sqlite)
|
||||||
|
|
||||||
security:
|
security:
|
||||||
password_salt: # CHANGE !
|
password_salt: # CHANGE !
|
||||||
|
@ -6,6 +6,7 @@ import (
|
|||||||
"fmt"
|
"fmt"
|
||||||
"github.com/gorilla/securecookie"
|
"github.com/gorilla/securecookie"
|
||||||
"github.com/jinzhu/configor"
|
"github.com/jinzhu/configor"
|
||||||
|
"github.com/markbates/pkger"
|
||||||
"github.com/muety/wakapi/models"
|
"github.com/muety/wakapi/models"
|
||||||
migrate "github.com/rubenv/sql-migrate"
|
migrate "github.com/rubenv/sql-migrate"
|
||||||
"gorm.io/driver/mysql"
|
"gorm.io/driver/mysql"
|
||||||
@ -27,6 +28,9 @@ const (
|
|||||||
SQLDialectMysql = "mysql"
|
SQLDialectMysql = "mysql"
|
||||||
SQLDialectPostgres = "postgres"
|
SQLDialectPostgres = "postgres"
|
||||||
SQLDialectSqlite = "sqlite3"
|
SQLDialectSqlite = "sqlite3"
|
||||||
|
|
||||||
|
KeyLatestTotalTime = "latest_total_time"
|
||||||
|
KeyLatestTotalUsers = "latest_total_users"
|
||||||
)
|
)
|
||||||
|
|
||||||
var cfg *Config
|
var cfg *Config
|
||||||
@ -34,6 +38,7 @@ var cFlag = flag.String("config", defaultConfigPath, "config file location")
|
|||||||
|
|
||||||
type appConfig struct {
|
type appConfig struct {
|
||||||
AggregationTime string `yaml:"aggregation_time" default:"02:15" env:"WAKAPI_AGGREGATION_TIME"`
|
AggregationTime string `yaml:"aggregation_time" default:"02:15" env:"WAKAPI_AGGREGATION_TIME"`
|
||||||
|
CountingTime string `yaml:"counting_time" default:"05:15" env:"WAKAPI_COUNTING_TIME"`
|
||||||
CustomLanguages map[string]string `yaml:"custom_languages"`
|
CustomLanguages map[string]string `yaml:"custom_languages"`
|
||||||
LanguageColors map[string]string `yaml:"-"`
|
LanguageColors map[string]string `yaml:"-"`
|
||||||
}
|
}
|
||||||
@ -52,8 +57,10 @@ type dbConfig struct {
|
|||||||
User string `env:"WAKAPI_DB_USER"`
|
User string `env:"WAKAPI_DB_USER"`
|
||||||
Password string `env:"WAKAPI_DB_PASSWORD"`
|
Password string `env:"WAKAPI_DB_PASSWORD"`
|
||||||
Name string `default:"wakapi_db.db" env:"WAKAPI_DB_NAME"`
|
Name string `default:"wakapi_db.db" env:"WAKAPI_DB_NAME"`
|
||||||
Dialect string `default:"sqlite3" env:"WAKAPI_DB_TYPE"`
|
Dialect string `yaml:"-"`
|
||||||
|
Type string `yaml:"dialect" default:"sqlite3" env:"WAKAPI_DB_TYPE"`
|
||||||
MaxConn uint `yaml:"max_conn" default:"2" env:"WAKAPI_DB_MAX_CONNECTIONS"`
|
MaxConn uint `yaml:"max_conn" default:"2" env:"WAKAPI_DB_MAX_CONNECTIONS"`
|
||||||
|
Ssl bool `default:"false" env:"WAKAPI_DB_SSL"`
|
||||||
}
|
}
|
||||||
|
|
||||||
type serverConfig struct {
|
type serverConfig struct {
|
||||||
@ -120,8 +127,8 @@ func (c *Config) GetMigrationFunc(dbDialect string) models.MigrationFunc {
|
|||||||
|
|
||||||
func (c *Config) GetFixturesFunc(dbDialect string) models.MigrationFunc {
|
func (c *Config) GetFixturesFunc(dbDialect string) models.MigrationFunc {
|
||||||
return func(db *gorm.DB) error {
|
return func(db *gorm.DB) error {
|
||||||
migrations := &migrate.FileMigrationSource{
|
migrations := &migrate.HttpFileSystemMigrationSource {
|
||||||
Dir: "migrations/common/fixtures",
|
FileSystem: pkger.Dir("/migrations"),
|
||||||
}
|
}
|
||||||
|
|
||||||
migrate.SetIgnoreUnknown(true)
|
migrate.SetIgnoreUnknown(true)
|
||||||
@ -166,12 +173,18 @@ func mysqlConnectionString(config *dbConfig) string {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func postgresConnectionString(config *dbConfig) string {
|
func postgresConnectionString(config *dbConfig) string {
|
||||||
return fmt.Sprintf("host=%s port=%d user=%s dbname=%s password=%s sslmode=disable",
|
sslmode := "disable"
|
||||||
|
if config.Ssl {
|
||||||
|
sslmode = "require"
|
||||||
|
}
|
||||||
|
|
||||||
|
return fmt.Sprintf("host=%s port=%d user=%s dbname=%s password=%s sslmode=%s",
|
||||||
config.Host,
|
config.Host,
|
||||||
config.Port,
|
config.Port,
|
||||||
config.User,
|
config.User,
|
||||||
config.Name,
|
config.Name,
|
||||||
config.Password,
|
config.Password,
|
||||||
|
sslmode,
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -179,12 +192,20 @@ func sqliteConnectionString(config *dbConfig) string {
|
|||||||
return config.Name
|
return config.Name
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (c *appConfig) GetCustomLanguages() map[string]string {
|
||||||
|
return cloneStringMap(c.CustomLanguages)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *appConfig) GetLanguageColors() map[string]string {
|
||||||
|
return cloneStringMap(c.LanguageColors)
|
||||||
|
}
|
||||||
|
|
||||||
func IsDev(env string) bool {
|
func IsDev(env string) bool {
|
||||||
return env == "dev" || env == "development"
|
return env == "dev" || env == "development"
|
||||||
}
|
}
|
||||||
|
|
||||||
func readVersion() string {
|
func readVersion() string {
|
||||||
file, err := os.Open("version.txt")
|
file, err := pkger.Open("/version.txt")
|
||||||
if err != nil {
|
if err != nil {
|
||||||
log.Fatal(err)
|
log.Fatal(err)
|
||||||
}
|
}
|
||||||
@ -195,7 +216,7 @@ func readVersion() string {
|
|||||||
log.Fatal(err)
|
log.Fatal(err)
|
||||||
}
|
}
|
||||||
|
|
||||||
return string(bytes)
|
return strings.TrimSpace(string(bytes))
|
||||||
}
|
}
|
||||||
|
|
||||||
func readLanguageColors() map[string]string {
|
func readLanguageColors() map[string]string {
|
||||||
@ -207,12 +228,17 @@ func readLanguageColors() map[string]string {
|
|||||||
Url string `json:"url"`
|
Url string `json:"url"`
|
||||||
}
|
}
|
||||||
|
|
||||||
data, err := ioutil.ReadFile("data/colors.json")
|
file, err := pkger.Open("/data/colors.json")
|
||||||
|
if err != nil {
|
||||||
|
log.Fatal(err)
|
||||||
|
}
|
||||||
|
defer file.Close()
|
||||||
|
bytes, err := ioutil.ReadAll(file)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
log.Fatal(err)
|
log.Fatal(err)
|
||||||
}
|
}
|
||||||
|
|
||||||
if err := json.Unmarshal(data, &rawColors); err != nil {
|
if err := json.Unmarshal(bytes, &rawColors); err != nil {
|
||||||
log.Fatal(err)
|
log.Fatal(err)
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -231,6 +257,13 @@ func mustReadConfigLocation() string {
|
|||||||
return *cFlag
|
return *cFlag
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func resolveDbDialect(dbType string) string {
|
||||||
|
if dbType == "cockroach" {
|
||||||
|
return "postgres"
|
||||||
|
}
|
||||||
|
return dbType
|
||||||
|
}
|
||||||
|
|
||||||
func Set(config *Config) {
|
func Set(config *Config) {
|
||||||
cfg = config
|
cfg = config
|
||||||
}
|
}
|
||||||
@ -252,6 +285,7 @@ func Load() *Config {
|
|||||||
|
|
||||||
config.Version = readVersion()
|
config.Version = readVersion()
|
||||||
config.App.LanguageColors = readLanguageColors()
|
config.App.LanguageColors = readLanguageColors()
|
||||||
|
config.Db.Dialect = resolveDbDialect(config.Db.Type)
|
||||||
config.Security.SecureCookie = securecookie.New(
|
config.Security.SecureCookie = securecookie.New(
|
||||||
securecookie.GenerateRandomKey(64),
|
securecookie.GenerateRandomKey(64),
|
||||||
securecookie.GenerateRandomKey(32),
|
securecookie.GenerateRandomKey(32),
|
||||||
|
9
config/utils.go
Normal file
9
config/utils.go
Normal file
@ -0,0 +1,9 @@
|
|||||||
|
package config
|
||||||
|
|
||||||
|
func cloneStringMap(m map[string]string) map[string]string {
|
||||||
|
m2 := make(map[string]string)
|
||||||
|
for k, v := range m {
|
||||||
|
m2[k] = v
|
||||||
|
}
|
||||||
|
return m2
|
||||||
|
}
|
@ -1,8 +1,4 @@
|
|||||||
mode: set
|
mode: set
|
||||||
github.com/muety/wakapi/models/language_mapping.go:11.42,13.2 1 0
|
|
||||||
github.com/muety/wakapi/models/language_mapping.go:15.51,17.2 1 0
|
|
||||||
github.com/muety/wakapi/models/language_mapping.go:19.52,21.2 1 0
|
|
||||||
github.com/muety/wakapi/models/models.go:3.14,5.2 0 1
|
|
||||||
github.com/muety/wakapi/models/filters.go:16.56,17.16 1 0
|
github.com/muety/wakapi/models/filters.go:16.56,17.16 1 0
|
||||||
github.com/muety/wakapi/models/filters.go:29.2,29.19 1 0
|
github.com/muety/wakapi/models/filters.go:29.2,29.19 1 0
|
||||||
github.com/muety/wakapi/models/filters.go:18.22,19.32 1 0
|
github.com/muety/wakapi/models/filters.go:18.22,19.32 1 0
|
||||||
@ -32,51 +28,9 @@ github.com/muety/wakapi/models/filters.go:53.20,55.3 1 0
|
|||||||
github.com/muety/wakapi/models/filters.go:56.22,58.3 1 1
|
github.com/muety/wakapi/models/filters.go:56.22,58.3 1 1
|
||||||
github.com/muety/wakapi/models/filters.go:59.21,61.3 1 0
|
github.com/muety/wakapi/models/filters.go:59.21,61.3 1 0
|
||||||
github.com/muety/wakapi/models/filters.go:62.16,64.3 1 0
|
github.com/muety/wakapi/models/filters.go:62.16,64.3 1 0
|
||||||
github.com/muety/wakapi/models/heartbeat.go:26.34,28.2 1 1
|
github.com/muety/wakapi/models/language_mapping.go:11.42,13.2 1 0
|
||||||
github.com/muety/wakapi/models/heartbeat.go:30.65,31.28 1 1
|
github.com/muety/wakapi/models/language_mapping.go:15.51,17.2 1 0
|
||||||
github.com/muety/wakapi/models/heartbeat.go:34.2,35.45 2 1
|
github.com/muety/wakapi/models/language_mapping.go:19.52,21.2 1 0
|
||||||
github.com/muety/wakapi/models/heartbeat.go:38.2,39.44 2 1
|
|
||||||
github.com/muety/wakapi/models/heartbeat.go:42.2,42.42 1 1
|
|
||||||
github.com/muety/wakapi/models/heartbeat.go:31.28,33.3 1 1
|
|
||||||
github.com/muety/wakapi/models/heartbeat.go:35.45,37.3 1 0
|
|
||||||
github.com/muety/wakapi/models/heartbeat.go:39.44,41.3 1 0
|
|
||||||
github.com/muety/wakapi/models/heartbeat.go:45.50,46.11 1 1
|
|
||||||
github.com/muety/wakapi/models/heartbeat.go:59.2,59.15 1 1
|
|
||||||
github.com/muety/wakapi/models/heartbeat.go:63.2,63.12 1 1
|
|
||||||
github.com/muety/wakapi/models/heartbeat.go:47.22,48.18 1 1
|
|
||||||
github.com/muety/wakapi/models/heartbeat.go:49.21,50.17 1 1
|
|
||||||
github.com/muety/wakapi/models/heartbeat.go:51.23,52.19 1 1
|
|
||||||
github.com/muety/wakapi/models/heartbeat.go:53.17,54.26 1 1
|
|
||||||
github.com/muety/wakapi/models/heartbeat.go:55.22,56.18 1 1
|
|
||||||
github.com/muety/wakapi/models/heartbeat.go:59.15,61.3 1 1
|
|
||||||
github.com/muety/wakapi/models/heartbeats.go:7.31,9.2 1 0
|
|
||||||
github.com/muety/wakapi/models/heartbeats.go:11.41,13.2 1 0
|
|
||||||
github.com/muety/wakapi/models/heartbeats.go:15.36,17.2 1 0
|
|
||||||
github.com/muety/wakapi/models/heartbeats.go:19.43,22.2 2 0
|
|
||||||
github.com/muety/wakapi/models/heartbeats.go:24.41,26.18 1 0
|
|
||||||
github.com/muety/wakapi/models/heartbeats.go:29.2,29.16 1 0
|
|
||||||
github.com/muety/wakapi/models/heartbeats.go:26.18,28.3 1 0
|
|
||||||
github.com/muety/wakapi/models/heartbeats.go:32.40,34.18 1 0
|
|
||||||
github.com/muety/wakapi/models/heartbeats.go:37.2,37.24 1 0
|
|
||||||
github.com/muety/wakapi/models/heartbeats.go:34.18,36.3 1 0
|
|
||||||
github.com/muety/wakapi/models/user.go:34.43,37.2 1 0
|
|
||||||
github.com/muety/wakapi/models/user.go:39.33,43.2 1 0
|
|
||||||
github.com/muety/wakapi/models/user.go:45.45,47.2 1 0
|
|
||||||
github.com/muety/wakapi/models/user.go:49.45,51.2 1 0
|
|
||||||
github.com/muety/wakapi/models/shared.go:34.52,37.16 3 0
|
|
||||||
github.com/muety/wakapi/models/shared.go:40.2,42.12 3 0
|
|
||||||
github.com/muety/wakapi/models/shared.go:37.16,39.3 1 0
|
|
||||||
github.com/muety/wakapi/models/shared.go:46.52,52.22 2 0
|
|
||||||
github.com/muety/wakapi/models/shared.go:68.2,71.12 3 0
|
|
||||||
github.com/muety/wakapi/models/shared.go:53.14,55.17 2 0
|
|
||||||
github.com/muety/wakapi/models/shared.go:58.13,60.8 2 0
|
|
||||||
github.com/muety/wakapi/models/shared.go:61.17,63.8 2 0
|
|
||||||
github.com/muety/wakapi/models/shared.go:64.10,65.64 1 0
|
|
||||||
github.com/muety/wakapi/models/shared.go:55.17,57.4 1 0
|
|
||||||
github.com/muety/wakapi/models/shared.go:74.51,77.2 2 0
|
|
||||||
github.com/muety/wakapi/models/shared.go:79.37,82.2 2 0
|
|
||||||
github.com/muety/wakapi/models/shared.go:84.35,86.2 1 0
|
|
||||||
github.com/muety/wakapi/models/shared.go:88.34,90.2 1 0
|
|
||||||
github.com/muety/wakapi/models/summary.go:29.27,33.2 1 0
|
github.com/muety/wakapi/models/summary.go:29.27,33.2 1 0
|
||||||
github.com/muety/wakapi/models/summary.go:83.29,85.2 1 1
|
github.com/muety/wakapi/models/summary.go:83.29,85.2 1 1
|
||||||
github.com/muety/wakapi/models/summary.go:87.37,94.2 6 1
|
github.com/muety/wakapi/models/summary.go:87.37,94.2 6 1
|
||||||
@ -127,28 +81,63 @@ github.com/muety/wakapi/models/summary.go:221.11,229.6 1 1
|
|||||||
github.com/muety/wakapi/models/summary.go:246.33,248.2 1 1
|
github.com/muety/wakapi/models/summary.go:246.33,248.2 1 1
|
||||||
github.com/muety/wakapi/models/summary.go:250.43,252.2 1 1
|
github.com/muety/wakapi/models/summary.go:250.43,252.2 1 1
|
||||||
github.com/muety/wakapi/models/summary.go:254.38,256.2 1 1
|
github.com/muety/wakapi/models/summary.go:254.38,256.2 1 1
|
||||||
github.com/muety/wakapi/utils/summary.go:10.71,13.18 2 0
|
github.com/muety/wakapi/models/user.go:34.43,37.2 1 0
|
||||||
github.com/muety/wakapi/utils/summary.go:37.2,37.22 1 0
|
github.com/muety/wakapi/models/user.go:39.33,43.2 1 0
|
||||||
github.com/muety/wakapi/utils/summary.go:14.28,15.24 1 0
|
github.com/muety/wakapi/models/user.go:45.45,47.2 1 0
|
||||||
github.com/muety/wakapi/utils/summary.go:16.32,18.22 2 0
|
github.com/muety/wakapi/models/user.go:49.45,51.2 1 0
|
||||||
github.com/muety/wakapi/utils/summary.go:19.31,20.23 1 0
|
github.com/muety/wakapi/models/alias.go:12.32,14.2 1 0
|
||||||
github.com/muety/wakapi/utils/summary.go:21.32,22.24 1 0
|
github.com/muety/wakapi/models/alias.go:16.37,17.35 1 0
|
||||||
github.com/muety/wakapi/utils/summary.go:23.31,24.23 1 0
|
github.com/muety/wakapi/models/alias.go:22.2,22.14 1 0
|
||||||
github.com/muety/wakapi/utils/summary.go:25.32,26.42 1 0
|
github.com/muety/wakapi/models/alias.go:17.35,18.18 1 0
|
||||||
github.com/muety/wakapi/utils/summary.go:27.33,28.43 1 0
|
github.com/muety/wakapi/models/alias.go:18.18,20.4 1 0
|
||||||
github.com/muety/wakapi/utils/summary.go:29.35,30.43 1 0
|
github.com/muety/wakapi/models/heartbeat.go:26.34,28.2 1 1
|
||||||
github.com/muety/wakapi/utils/summary.go:31.26,32.21 1 0
|
github.com/muety/wakapi/models/heartbeat.go:30.65,31.28 1 1
|
||||||
github.com/muety/wakapi/utils/summary.go:33.10,34.39 1 0
|
github.com/muety/wakapi/models/heartbeat.go:34.2,35.45 2 1
|
||||||
github.com/muety/wakapi/utils/summary.go:40.73,47.56 5 0
|
github.com/muety/wakapi/models/heartbeat.go:38.2,39.44 2 1
|
||||||
github.com/muety/wakapi/utils/summary.go:61.2,68.8 2 0
|
github.com/muety/wakapi/models/heartbeat.go:42.2,42.42 1 1
|
||||||
github.com/muety/wakapi/utils/summary.go:47.56,49.3 1 0
|
github.com/muety/wakapi/models/heartbeat.go:31.28,33.3 1 1
|
||||||
github.com/muety/wakapi/utils/summary.go:49.8,51.17 2 0
|
github.com/muety/wakapi/models/heartbeat.go:35.45,37.3 1 0
|
||||||
github.com/muety/wakapi/utils/summary.go:55.3,56.17 2 0
|
github.com/muety/wakapi/models/heartbeat.go:39.44,41.3 1 0
|
||||||
github.com/muety/wakapi/utils/summary.go:51.17,53.4 1 0
|
github.com/muety/wakapi/models/heartbeat.go:45.50,46.11 1 1
|
||||||
github.com/muety/wakapi/utils/summary.go:56.17,58.4 1 0
|
github.com/muety/wakapi/models/heartbeat.go:59.2,59.15 1 1
|
||||||
|
github.com/muety/wakapi/models/heartbeat.go:63.2,63.12 1 1
|
||||||
|
github.com/muety/wakapi/models/heartbeat.go:47.22,48.18 1 1
|
||||||
|
github.com/muety/wakapi/models/heartbeat.go:49.21,50.17 1 1
|
||||||
|
github.com/muety/wakapi/models/heartbeat.go:51.23,52.19 1 1
|
||||||
|
github.com/muety/wakapi/models/heartbeat.go:53.17,54.26 1 1
|
||||||
|
github.com/muety/wakapi/models/heartbeat.go:55.22,56.18 1 1
|
||||||
|
github.com/muety/wakapi/models/heartbeat.go:59.15,61.3 1 1
|
||||||
|
github.com/muety/wakapi/models/heartbeats.go:7.31,9.2 1 0
|
||||||
|
github.com/muety/wakapi/models/heartbeats.go:11.41,13.2 1 0
|
||||||
|
github.com/muety/wakapi/models/heartbeats.go:15.36,17.2 1 0
|
||||||
|
github.com/muety/wakapi/models/heartbeats.go:19.43,22.2 2 0
|
||||||
|
github.com/muety/wakapi/models/heartbeats.go:24.41,26.18 1 0
|
||||||
|
github.com/muety/wakapi/models/heartbeats.go:29.2,29.16 1 0
|
||||||
|
github.com/muety/wakapi/models/heartbeats.go:26.18,28.3 1 0
|
||||||
|
github.com/muety/wakapi/models/heartbeats.go:32.40,34.18 1 0
|
||||||
|
github.com/muety/wakapi/models/heartbeats.go:37.2,37.24 1 0
|
||||||
|
github.com/muety/wakapi/models/heartbeats.go:34.18,36.3 1 0
|
||||||
|
github.com/muety/wakapi/models/models.go:3.14,5.2 0 1
|
||||||
|
github.com/muety/wakapi/models/shared.go:34.52,37.16 3 0
|
||||||
|
github.com/muety/wakapi/models/shared.go:40.2,42.12 3 0
|
||||||
|
github.com/muety/wakapi/models/shared.go:37.16,39.3 1 0
|
||||||
|
github.com/muety/wakapi/models/shared.go:46.52,52.22 2 0
|
||||||
|
github.com/muety/wakapi/models/shared.go:68.2,71.12 3 0
|
||||||
|
github.com/muety/wakapi/models/shared.go:53.14,55.17 2 0
|
||||||
|
github.com/muety/wakapi/models/shared.go:58.13,60.8 2 0
|
||||||
|
github.com/muety/wakapi/models/shared.go:61.17,63.8 2 0
|
||||||
|
github.com/muety/wakapi/models/shared.go:64.10,65.64 1 0
|
||||||
|
github.com/muety/wakapi/models/shared.go:55.17,57.4 1 0
|
||||||
|
github.com/muety/wakapi/models/shared.go:74.51,77.2 2 0
|
||||||
|
github.com/muety/wakapi/models/shared.go:79.37,82.2 2 0
|
||||||
|
github.com/muety/wakapi/models/shared.go:84.35,86.2 1 0
|
||||||
|
github.com/muety/wakapi/models/shared.go:88.34,90.2 1 0
|
||||||
github.com/muety/wakapi/utils/template.go:8.41,10.16 2 0
|
github.com/muety/wakapi/utils/template.go:8.41,10.16 2 0
|
||||||
github.com/muety/wakapi/utils/template.go:13.2,13.23 1 0
|
github.com/muety/wakapi/utils/template.go:13.2,13.23 1 0
|
||||||
github.com/muety/wakapi/utils/template.go:10.16,12.3 1 0
|
github.com/muety/wakapi/utils/template.go:10.16,12.3 1 0
|
||||||
|
github.com/muety/wakapi/utils/template.go:16.37,17.30 1 0
|
||||||
|
github.com/muety/wakapi/utils/template.go:20.2,20.10 1 0
|
||||||
|
github.com/muety/wakapi/utils/template.go:17.30,19.3 1 0
|
||||||
github.com/muety/wakapi/utils/auth.go:18.79,20.54 2 0
|
github.com/muety/wakapi/utils/auth.go:18.79,20.54 2 0
|
||||||
github.com/muety/wakapi/utils/auth.go:24.2,26.16 3 0
|
github.com/muety/wakapi/utils/auth.go:24.2,26.16 3 0
|
||||||
github.com/muety/wakapi/utils/auth.go:30.2,32.45 3 0
|
github.com/muety/wakapi/utils/auth.go:30.2,32.45 3 0
|
||||||
@ -178,9 +167,10 @@ github.com/muety/wakapi/utils/color.go:11.50,13.4 1 0
|
|||||||
github.com/muety/wakapi/utils/common.go:9.48,11.2 1 0
|
github.com/muety/wakapi/utils/common.go:9.48,11.2 1 0
|
||||||
github.com/muety/wakapi/utils/common.go:13.40,15.2 1 0
|
github.com/muety/wakapi/utils/common.go:13.40,15.2 1 0
|
||||||
github.com/muety/wakapi/utils/common.go:17.45,19.2 1 0
|
github.com/muety/wakapi/utils/common.go:17.45,19.2 1 0
|
||||||
github.com/muety/wakapi/utils/common.go:21.56,24.45 3 1
|
github.com/muety/wakapi/utils/common.go:21.24,23.2 1 0
|
||||||
github.com/muety/wakapi/utils/common.go:27.2,27.40 1 1
|
github.com/muety/wakapi/utils/common.go:25.56,28.45 3 1
|
||||||
github.com/muety/wakapi/utils/common.go:24.45,26.3 1 1
|
github.com/muety/wakapi/utils/common.go:31.2,31.40 1 1
|
||||||
|
github.com/muety/wakapi/utils/common.go:28.45,30.3 1 1
|
||||||
github.com/muety/wakapi/utils/date.go:8.31,10.2 1 0
|
github.com/muety/wakapi/utils/date.go:8.31,10.2 1 0
|
||||||
github.com/muety/wakapi/utils/date.go:12.43,14.2 1 0
|
github.com/muety/wakapi/utils/date.go:12.43,14.2 1 0
|
||||||
github.com/muety/wakapi/utils/date.go:16.30,20.2 3 0
|
github.com/muety/wakapi/utils/date.go:16.30,20.2 3 0
|
||||||
@ -206,54 +196,80 @@ github.com/muety/wakapi/utils/strings.go:12.77,13.29 1 0
|
|||||||
github.com/muety/wakapi/utils/strings.go:18.2,18.19 1 0
|
github.com/muety/wakapi/utils/strings.go:18.2,18.19 1 0
|
||||||
github.com/muety/wakapi/utils/strings.go:13.29,14.18 1 0
|
github.com/muety/wakapi/utils/strings.go:13.29,14.18 1 0
|
||||||
github.com/muety/wakapi/utils/strings.go:14.18,16.4 1 0
|
github.com/muety/wakapi/utils/strings.go:14.18,16.4 1 0
|
||||||
github.com/muety/wakapi/config/config.go:77.70,79.2 1 0
|
github.com/muety/wakapi/utils/summary.go:10.71,13.18 2 0
|
||||||
github.com/muety/wakapi/config/config.go:81.65,83.2 1 0
|
github.com/muety/wakapi/utils/summary.go:37.2,37.22 1 0
|
||||||
github.com/muety/wakapi/config/config.go:85.82,95.2 1 0
|
github.com/muety/wakapi/utils/summary.go:14.28,15.24 1 0
|
||||||
github.com/muety/wakapi/config/config.go:97.31,99.2 1 0
|
github.com/muety/wakapi/utils/summary.go:16.32,18.22 2 0
|
||||||
github.com/muety/wakapi/config/config.go:101.32,103.2 1 0
|
github.com/muety/wakapi/utils/summary.go:19.31,20.23 1 0
|
||||||
github.com/muety/wakapi/config/config.go:105.74,106.19 1 0
|
github.com/muety/wakapi/utils/summary.go:21.32,22.24 1 0
|
||||||
github.com/muety/wakapi/config/config.go:107.10,108.34 1 0
|
github.com/muety/wakapi/utils/summary.go:23.31,24.23 1 0
|
||||||
github.com/muety/wakapi/config/config.go:108.34,117.4 8 0
|
github.com/muety/wakapi/utils/summary.go:25.32,26.42 1 0
|
||||||
github.com/muety/wakapi/config/config.go:121.73,122.33 1 0
|
github.com/muety/wakapi/utils/summary.go:27.33,28.43 1 0
|
||||||
github.com/muety/wakapi/config/config.go:122.33,130.17 5 0
|
github.com/muety/wakapi/utils/summary.go:29.35,30.43 1 0
|
||||||
github.com/muety/wakapi/config/config.go:134.3,135.13 2 0
|
github.com/muety/wakapi/utils/summary.go:31.26,32.21 1 0
|
||||||
github.com/muety/wakapi/config/config.go:130.17,132.4 1 0
|
github.com/muety/wakapi/utils/summary.go:33.10,34.39 1 0
|
||||||
github.com/muety/wakapi/config/config.go:139.50,140.19 1 0
|
github.com/muety/wakapi/utils/summary.go:40.73,47.56 5 0
|
||||||
github.com/muety/wakapi/config/config.go:153.2,153.12 1 0
|
github.com/muety/wakapi/utils/summary.go:61.2,68.8 2 0
|
||||||
github.com/muety/wakapi/config/config.go:141.23,145.5 1 0
|
github.com/muety/wakapi/utils/summary.go:47.56,49.3 1 0
|
||||||
github.com/muety/wakapi/config/config.go:146.26,149.5 1 0
|
github.com/muety/wakapi/utils/summary.go:49.8,51.17 2 0
|
||||||
github.com/muety/wakapi/config/config.go:150.24,151.48 1 0
|
github.com/muety/wakapi/utils/summary.go:55.3,56.17 2 0
|
||||||
github.com/muety/wakapi/config/config.go:156.53,166.2 1 1
|
github.com/muety/wakapi/utils/summary.go:51.17,53.4 1 0
|
||||||
github.com/muety/wakapi/config/config.go:168.56,176.2 1 1
|
github.com/muety/wakapi/utils/summary.go:56.17,58.4 1 0
|
||||||
github.com/muety/wakapi/config/config.go:178.54,180.2 1 1
|
github.com/muety/wakapi/config/config.go:83.70,85.2 1 0
|
||||||
github.com/muety/wakapi/config/config.go:182.29,184.2 1 1
|
github.com/muety/wakapi/config/config.go:87.65,89.2 1 0
|
||||||
github.com/muety/wakapi/config/config.go:186.27,188.16 2 0
|
github.com/muety/wakapi/config/config.go:91.82,101.2 1 0
|
||||||
github.com/muety/wakapi/config/config.go:191.2,194.16 3 0
|
github.com/muety/wakapi/config/config.go:103.31,105.2 1 0
|
||||||
github.com/muety/wakapi/config/config.go:198.2,198.22 1 0
|
github.com/muety/wakapi/config/config.go:107.32,109.2 1 0
|
||||||
github.com/muety/wakapi/config/config.go:188.16,190.3 1 0
|
github.com/muety/wakapi/config/config.go:111.74,112.19 1 0
|
||||||
github.com/muety/wakapi/config/config.go:194.16,196.3 1 0
|
github.com/muety/wakapi/config/config.go:113.10,114.34 1 0
|
||||||
github.com/muety/wakapi/config/config.go:201.45,211.16 4 0
|
github.com/muety/wakapi/config/config.go:114.34,123.4 8 0
|
||||||
github.com/muety/wakapi/config/config.go:215.2,215.57 1 0
|
github.com/muety/wakapi/config/config.go:127.73,128.33 1 0
|
||||||
github.com/muety/wakapi/config/config.go:219.2,219.30 1 0
|
github.com/muety/wakapi/config/config.go:128.33,136.17 5 0
|
||||||
github.com/muety/wakapi/config/config.go:223.2,223.15 1 0
|
github.com/muety/wakapi/config/config.go:140.3,141.13 2 0
|
||||||
github.com/muety/wakapi/config/config.go:211.16,213.3 1 0
|
github.com/muety/wakapi/config/config.go:136.17,138.4 1 0
|
||||||
github.com/muety/wakapi/config/config.go:215.57,217.3 1 0
|
github.com/muety/wakapi/config/config.go:145.50,146.19 1 0
|
||||||
github.com/muety/wakapi/config/config.go:219.30,221.3 1 0
|
github.com/muety/wakapi/config/config.go:159.2,159.12 1 0
|
||||||
github.com/muety/wakapi/config/config.go:226.38,227.43 1 0
|
github.com/muety/wakapi/config/config.go:147.23,151.5 1 0
|
||||||
github.com/muety/wakapi/config/config.go:231.2,231.15 1 0
|
github.com/muety/wakapi/config/config.go:152.26,155.5 1 0
|
||||||
github.com/muety/wakapi/config/config.go:227.43,229.3 1 0
|
github.com/muety/wakapi/config/config.go:156.24,157.48 1 0
|
||||||
github.com/muety/wakapi/config/config.go:234.26,236.2 1 0
|
github.com/muety/wakapi/config/config.go:162.53,172.2 1 1
|
||||||
github.com/muety/wakapi/config/config.go:238.20,240.2 1 0
|
github.com/muety/wakapi/config/config.go:174.56,176.16 2 1
|
||||||
github.com/muety/wakapi/config/config.go:242.21,249.96 4 0
|
github.com/muety/wakapi/config/config.go:180.2,187.3 1 1
|
||||||
github.com/muety/wakapi/config/config.go:253.2,260.52 4 0
|
github.com/muety/wakapi/config/config.go:176.16,178.3 1 0
|
||||||
github.com/muety/wakapi/config/config.go:264.2,264.47 1 0
|
github.com/muety/wakapi/config/config.go:190.54,192.2 1 1
|
||||||
github.com/muety/wakapi/config/config.go:270.2,270.70 1 0
|
github.com/muety/wakapi/config/config.go:194.60,196.2 1 0
|
||||||
github.com/muety/wakapi/config/config.go:274.2,275.14 2 0
|
github.com/muety/wakapi/config/config.go:198.59,200.2 1 0
|
||||||
github.com/muety/wakapi/config/config.go:249.96,251.3 1 0
|
github.com/muety/wakapi/config/config.go:202.29,204.2 1 1
|
||||||
github.com/muety/wakapi/config/config.go:260.52,262.3 1 0
|
github.com/muety/wakapi/config/config.go:206.27,208.16 2 0
|
||||||
github.com/muety/wakapi/config/config.go:264.47,265.14 1 0
|
github.com/muety/wakapi/config/config.go:211.2,214.16 3 0
|
||||||
github.com/muety/wakapi/config/config.go:265.14,267.4 1 0
|
github.com/muety/wakapi/config/config.go:218.2,218.22 1 0
|
||||||
github.com/muety/wakapi/config/config.go:270.70,272.3 1 0
|
github.com/muety/wakapi/config/config.go:208.16,210.3 1 0
|
||||||
|
github.com/muety/wakapi/config/config.go:214.16,216.3 1 0
|
||||||
|
github.com/muety/wakapi/config/config.go:221.45,231.16 4 0
|
||||||
|
github.com/muety/wakapi/config/config.go:235.2,235.57 1 0
|
||||||
|
github.com/muety/wakapi/config/config.go:239.2,239.30 1 0
|
||||||
|
github.com/muety/wakapi/config/config.go:243.2,243.15 1 0
|
||||||
|
github.com/muety/wakapi/config/config.go:231.16,233.3 1 0
|
||||||
|
github.com/muety/wakapi/config/config.go:235.57,237.3 1 0
|
||||||
|
github.com/muety/wakapi/config/config.go:239.30,241.3 1 0
|
||||||
|
github.com/muety/wakapi/config/config.go:246.38,247.43 1 0
|
||||||
|
github.com/muety/wakapi/config/config.go:251.2,251.15 1 0
|
||||||
|
github.com/muety/wakapi/config/config.go:247.43,249.3 1 0
|
||||||
|
github.com/muety/wakapi/config/config.go:254.45,255.27 1 0
|
||||||
|
github.com/muety/wakapi/config/config.go:258.2,258.15 1 0
|
||||||
|
github.com/muety/wakapi/config/config.go:255.27,257.3 1 0
|
||||||
|
github.com/muety/wakapi/config/config.go:261.26,263.2 1 0
|
||||||
|
github.com/muety/wakapi/config/config.go:265.20,267.2 1 0
|
||||||
|
github.com/muety/wakapi/config/config.go:269.21,276.96 4 0
|
||||||
|
github.com/muety/wakapi/config/config.go:280.2,288.52 5 0
|
||||||
|
github.com/muety/wakapi/config/config.go:292.2,292.47 1 0
|
||||||
|
github.com/muety/wakapi/config/config.go:298.2,298.70 1 0
|
||||||
|
github.com/muety/wakapi/config/config.go:302.2,303.14 2 0
|
||||||
|
github.com/muety/wakapi/config/config.go:276.96,278.3 1 0
|
||||||
|
github.com/muety/wakapi/config/config.go:288.52,290.3 1 0
|
||||||
|
github.com/muety/wakapi/config/config.go:292.47,293.14 1 0
|
||||||
|
github.com/muety/wakapi/config/config.go:293.14,295.4 1 0
|
||||||
|
github.com/muety/wakapi/config/config.go:298.70,300.3 1 0
|
||||||
github.com/muety/wakapi/config/legacy.go:13.33,14.57 1 0
|
github.com/muety/wakapi/config/legacy.go:13.33,14.57 1 0
|
||||||
github.com/muety/wakapi/config/legacy.go:14.57,16.3 1 0
|
github.com/muety/wakapi/config/legacy.go:14.57,16.3 1 0
|
||||||
github.com/muety/wakapi/config/legacy.go:16.8,16.16 1 0
|
github.com/muety/wakapi/config/legacy.go:16.8,16.16 1 0
|
||||||
@ -284,6 +300,11 @@ github.com/muety/wakapi/config/legacy.go:75.23,77.3 1 0
|
|||||||
github.com/muety/wakapi/config/legacy.go:82.33,84.3 1 0
|
github.com/muety/wakapi/config/legacy.go:82.33,84.3 1 0
|
||||||
github.com/muety/wakapi/config/legacy.go:114.16,116.3 1 0
|
github.com/muety/wakapi/config/legacy.go:114.16,116.3 1 0
|
||||||
github.com/muety/wakapi/config/legacy.go:119.78,121.3 1 0
|
github.com/muety/wakapi/config/legacy.go:119.78,121.3 1 0
|
||||||
|
github.com/muety/wakapi/config/utils.go:3.60,5.22 2 0
|
||||||
|
github.com/muety/wakapi/config/utils.go:8.2,8.11 1 0
|
||||||
|
github.com/muety/wakapi/config/utils.go:5.22,7.3 1 0
|
||||||
|
github.com/muety/wakapi/middlewares/logging.go:11.48,13.2 1 0
|
||||||
|
github.com/muety/wakapi/middlewares/logging.go:15.66,17.2 1 0
|
||||||
github.com/muety/wakapi/middlewares/authenticate.go:27.116,34.2 1 1
|
github.com/muety/wakapi/middlewares/authenticate.go:27.116,34.2 1 1
|
||||||
github.com/muety/wakapi/middlewares/authenticate.go:36.71,37.71 1 0
|
github.com/muety/wakapi/middlewares/authenticate.go:36.71,37.71 1 0
|
||||||
github.com/muety/wakapi/middlewares/authenticate.go:37.71,39.3 1 0
|
github.com/muety/wakapi/middlewares/authenticate.go:37.71,39.3 1 0
|
||||||
@ -319,34 +340,38 @@ github.com/muety/wakapi/middlewares/authenticate.go:127.2,127.65 1 0
|
|||||||
github.com/muety/wakapi/middlewares/authenticate.go:119.32,120.58 1 0
|
github.com/muety/wakapi/middlewares/authenticate.go:119.32,120.58 1 0
|
||||||
github.com/muety/wakapi/middlewares/authenticate.go:125.3,125.15 1 0
|
github.com/muety/wakapi/middlewares/authenticate.go:125.3,125.15 1 0
|
||||||
github.com/muety/wakapi/middlewares/authenticate.go:120.58,124.4 3 0
|
github.com/muety/wakapi/middlewares/authenticate.go:120.58,124.4 3 0
|
||||||
github.com/muety/wakapi/middlewares/logging.go:11.48,13.2 1 0
|
github.com/muety/wakapi/services/misc.go:23.126,30.2 1 0
|
||||||
github.com/muety/wakapi/middlewares/logging.go:15.66,17.2 1 0
|
github.com/muety/wakapi/services/misc.go:42.50,44.48 1 0
|
||||||
github.com/muety/wakapi/services/language_mapping.go:17.118,23.2 1 0
|
github.com/muety/wakapi/services/misc.go:48.2,50.19 3 0
|
||||||
github.com/muety/wakapi/services/language_mapping.go:25.86,27.2 1 0
|
github.com/muety/wakapi/services/misc.go:44.48,46.3 1 0
|
||||||
github.com/muety/wakapi/services/language_mapping.go:29.96,30.53 1 0
|
github.com/muety/wakapi/services/misc.go:53.51,59.40 4 0
|
||||||
github.com/muety/wakapi/services/language_mapping.go:34.2,35.16 2 0
|
github.com/muety/wakapi/services/misc.go:63.2,66.56 2 0
|
||||||
github.com/muety/wakapi/services/language_mapping.go:38.2,39.22 2 0
|
github.com/muety/wakapi/services/misc.go:77.2,77.12 1 0
|
||||||
github.com/muety/wakapi/services/language_mapping.go:30.53,32.3 1 0
|
github.com/muety/wakapi/services/misc.go:59.40,61.3 1 0
|
||||||
github.com/muety/wakapi/services/language_mapping.go:35.16,37.3 1 0
|
github.com/muety/wakapi/services/misc.go:66.56,67.27 1 0
|
||||||
github.com/muety/wakapi/services/language_mapping.go:42.92,45.16 3 0
|
github.com/muety/wakapi/services/misc.go:67.27,72.4 1 0
|
||||||
github.com/muety/wakapi/services/language_mapping.go:49.2,49.33 1 0
|
github.com/muety/wakapi/services/misc.go:73.8,75.3 1 0
|
||||||
github.com/muety/wakapi/services/language_mapping.go:52.2,52.22 1 0
|
github.com/muety/wakapi/services/misc.go:80.116,81.24 1 0
|
||||||
github.com/muety/wakapi/services/language_mapping.go:45.16,47.3 1 0
|
github.com/muety/wakapi/services/misc.go:81.24,82.144 1 0
|
||||||
github.com/muety/wakapi/services/language_mapping.go:49.33,51.3 1 0
|
github.com/muety/wakapi/services/misc.go:91.3,91.48 1 0
|
||||||
github.com/muety/wakapi/services/language_mapping.go:55.109,57.16 2 0
|
github.com/muety/wakapi/services/misc.go:82.144,84.4 1 0
|
||||||
github.com/muety/wakapi/services/language_mapping.go:61.2,62.20 2 0
|
github.com/muety/wakapi/services/misc.go:84.9,90.4 2 0
|
||||||
github.com/muety/wakapi/services/language_mapping.go:57.16,59.3 1 0
|
github.com/muety/wakapi/services/misc.go:91.48,94.4 2 0
|
||||||
github.com/muety/wakapi/services/language_mapping.go:65.82,69.2 3 0
|
github.com/muety/wakapi/services/misc.go:98.86,101.30 3 0
|
||||||
github.com/muety/wakapi/services/language_mapping.go:71.73,73.2 1 0
|
github.com/muety/wakapi/services/misc.go:106.2,109.17 1 0
|
||||||
|
github.com/muety/wakapi/services/misc.go:113.2,116.17 1 0
|
||||||
|
github.com/muety/wakapi/services/misc.go:101.30,104.3 2 0
|
||||||
|
github.com/muety/wakapi/services/misc.go:109.17,111.3 1 0
|
||||||
|
github.com/muety/wakapi/services/misc.go:116.17,118.3 1 0
|
||||||
github.com/muety/wakapi/services/summary.go:27.149,35.2 1 1
|
github.com/muety/wakapi/services/summary.go:27.149,35.2 1 1
|
||||||
github.com/muety/wakapi/services/summary.go:39.120,42.52 2 1
|
github.com/muety/wakapi/services/summary.go:39.120,42.52 2 1
|
||||||
github.com/muety/wakapi/services/summary.go:47.2,47.44 1 1
|
github.com/muety/wakapi/services/summary.go:47.2,47.44 1 1
|
||||||
github.com/muety/wakapi/services/summary.go:53.2,53.66 1 1
|
github.com/muety/wakapi/services/summary.go:53.2,53.65 1 1
|
||||||
github.com/muety/wakapi/services/summary.go:58.2,59.16 2 1
|
github.com/muety/wakapi/services/summary.go:58.2,59.16 2 1
|
||||||
github.com/muety/wakapi/services/summary.go:64.2,66.30 3 1
|
github.com/muety/wakapi/services/summary.go:64.2,66.30 3 1
|
||||||
github.com/muety/wakapi/services/summary.go:42.52,44.3 1 0
|
github.com/muety/wakapi/services/summary.go:42.52,44.3 1 0
|
||||||
github.com/muety/wakapi/services/summary.go:47.44,50.3 2 1
|
github.com/muety/wakapi/services/summary.go:47.44,50.3 2 1
|
||||||
github.com/muety/wakapi/services/summary.go:53.66,55.3 1 0
|
github.com/muety/wakapi/services/summary.go:53.65,55.3 1 0
|
||||||
github.com/muety/wakapi/services/summary.go:59.16,61.3 1 0
|
github.com/muety/wakapi/services/summary.go:59.16,61.3 1 0
|
||||||
github.com/muety/wakapi/services/summary.go:69.101,72.52 2 1
|
github.com/muety/wakapi/services/summary.go:69.101,72.52 2 1
|
||||||
github.com/muety/wakapi/services/summary.go:77.2,78.16 2 1
|
github.com/muety/wakapi/services/summary.go:77.2,78.16 2 1
|
||||||
@ -442,6 +467,64 @@ github.com/muety/wakapi/services/user.go:64.106,66.96 2 0
|
|||||||
github.com/muety/wakapi/services/user.go:71.2,71.68 1 0
|
github.com/muety/wakapi/services/user.go:71.2,71.68 1 0
|
||||||
github.com/muety/wakapi/services/user.go:66.96,68.3 1 0
|
github.com/muety/wakapi/services/user.go:66.96,68.3 1 0
|
||||||
github.com/muety/wakapi/services/user.go:68.8,70.3 1 0
|
github.com/muety/wakapi/services/user.go:68.8,70.3 1 0
|
||||||
|
github.com/muety/wakapi/services/alias.go:17.77,22.2 1 1
|
||||||
|
github.com/muety/wakapi/services/alias.go:26.60,27.43 1 1
|
||||||
|
github.com/muety/wakapi/services/alias.go:30.2,30.14 1 1
|
||||||
|
github.com/muety/wakapi/services/alias.go:27.43,29.3 1 1
|
||||||
|
github.com/muety/wakapi/services/alias.go:33.62,35.16 2 1
|
||||||
|
github.com/muety/wakapi/services/alias.go:38.2,38.12 1 1
|
||||||
|
github.com/muety/wakapi/services/alias.go:35.16,37.3 1 1
|
||||||
|
github.com/muety/wakapi/services/alias.go:41.76,43.16 2 0
|
||||||
|
github.com/muety/wakapi/services/alias.go:46.2,46.21 1 0
|
||||||
|
github.com/muety/wakapi/services/alias.go:43.16,45.3 1 0
|
||||||
|
github.com/muety/wakapi/services/alias.go:49.113,51.16 2 0
|
||||||
|
github.com/muety/wakapi/services/alias.go:54.2,54.21 1 0
|
||||||
|
github.com/muety/wakapi/services/alias.go:51.16,53.3 1 0
|
||||||
|
github.com/muety/wakapi/services/alias.go:57.108,58.32 1 1
|
||||||
|
github.com/muety/wakapi/services/alias.go:64.2,65.46 2 1
|
||||||
|
github.com/muety/wakapi/services/alias.go:70.2,70.19 1 1
|
||||||
|
github.com/muety/wakapi/services/alias.go:58.32,59.52 1 1
|
||||||
|
github.com/muety/wakapi/services/alias.go:59.52,61.4 1 1
|
||||||
|
github.com/muety/wakapi/services/alias.go:65.46,66.48 1 1
|
||||||
|
github.com/muety/wakapi/services/alias.go:66.48,68.4 1 1
|
||||||
|
github.com/muety/wakapi/services/alias.go:73.77,75.16 2 0
|
||||||
|
github.com/muety/wakapi/services/alias.go:78.2,79.20 2 0
|
||||||
|
github.com/muety/wakapi/services/alias.go:75.16,77.3 1 0
|
||||||
|
github.com/muety/wakapi/services/alias.go:82.60,83.24 1 0
|
||||||
|
github.com/muety/wakapi/services/alias.go:86.2,88.12 3 0
|
||||||
|
github.com/muety/wakapi/services/alias.go:83.24,85.3 1 0
|
||||||
|
github.com/muety/wakapi/services/alias.go:91.69,94.28 3 0
|
||||||
|
github.com/muety/wakapi/services/alias.go:102.2,104.31 2 0
|
||||||
|
github.com/muety/wakapi/services/alias.go:108.2,108.12 1 0
|
||||||
|
github.com/muety/wakapi/services/alias.go:94.28,95.21 1 0
|
||||||
|
github.com/muety/wakapi/services/alias.go:98.3,99.16 2 0
|
||||||
|
github.com/muety/wakapi/services/alias.go:95.21,97.4 1 0
|
||||||
|
github.com/muety/wakapi/services/alias.go:104.31,106.3 1 0
|
||||||
|
github.com/muety/wakapi/services/alias.go:111.52,112.51 1 0
|
||||||
|
github.com/muety/wakapi/services/alias.go:112.51,114.3 1 0
|
||||||
|
github.com/muety/wakapi/services/key_value.go:14.89,19.2 1 0
|
||||||
|
github.com/muety/wakapi/services/key_value.go:21.83,23.2 1 0
|
||||||
|
github.com/muety/wakapi/services/key_value.go:25.72,27.2 1 0
|
||||||
|
github.com/muety/wakapi/services/key_value.go:29.60,31.2 1 0
|
||||||
|
github.com/muety/wakapi/services/language_mapping.go:18.118,24.2 1 0
|
||||||
|
github.com/muety/wakapi/services/language_mapping.go:26.86,28.2 1 0
|
||||||
|
github.com/muety/wakapi/services/language_mapping.go:30.96,31.53 1 0
|
||||||
|
github.com/muety/wakapi/services/language_mapping.go:35.2,36.16 2 0
|
||||||
|
github.com/muety/wakapi/services/language_mapping.go:39.2,40.22 2 0
|
||||||
|
github.com/muety/wakapi/services/language_mapping.go:31.53,33.3 1 0
|
||||||
|
github.com/muety/wakapi/services/language_mapping.go:36.16,38.3 1 0
|
||||||
|
github.com/muety/wakapi/services/language_mapping.go:43.92,46.16 3 0
|
||||||
|
github.com/muety/wakapi/services/language_mapping.go:50.2,50.33 1 0
|
||||||
|
github.com/muety/wakapi/services/language_mapping.go:53.2,53.22 1 0
|
||||||
|
github.com/muety/wakapi/services/language_mapping.go:46.16,48.3 1 0
|
||||||
|
github.com/muety/wakapi/services/language_mapping.go:50.33,52.3 1 0
|
||||||
|
github.com/muety/wakapi/services/language_mapping.go:56.109,58.16 2 0
|
||||||
|
github.com/muety/wakapi/services/language_mapping.go:62.2,63.20 2 0
|
||||||
|
github.com/muety/wakapi/services/language_mapping.go:58.16,60.3 1 0
|
||||||
|
github.com/muety/wakapi/services/language_mapping.go:66.82,67.26 1 0
|
||||||
|
github.com/muety/wakapi/services/language_mapping.go:70.2,72.12 3 0
|
||||||
|
github.com/muety/wakapi/services/language_mapping.go:67.26,69.3 1 0
|
||||||
|
github.com/muety/wakapi/services/language_mapping.go:75.74,78.2 1 0
|
||||||
github.com/muety/wakapi/services/aggregation.go:24.142,31.2 1 0
|
github.com/muety/wakapi/services/aggregation.go:24.142,31.2 1 0
|
||||||
github.com/muety/wakapi/services/aggregation.go:40.43,42.37 1 0
|
github.com/muety/wakapi/services/aggregation.go:40.43,42.37 1 0
|
||||||
github.com/muety/wakapi/services/aggregation.go:46.2,48.19 3 0
|
github.com/muety/wakapi/services/aggregation.go:46.2,48.19 3 0
|
||||||
@ -482,20 +565,6 @@ github.com/muety/wakapi/services/aggregation.go:136.62,140.4 1 0
|
|||||||
github.com/muety/wakapi/services/aggregation.go:148.83,163.41 5 0
|
github.com/muety/wakapi/services/aggregation.go:148.83,163.41 5 0
|
||||||
github.com/muety/wakapi/services/aggregation.go:163.41,173.3 3 0
|
github.com/muety/wakapi/services/aggregation.go:163.41,173.3 3 0
|
||||||
github.com/muety/wakapi/services/aggregation.go:176.34,179.2 2 0
|
github.com/muety/wakapi/services/aggregation.go:176.34,179.2 2 0
|
||||||
github.com/muety/wakapi/services/alias.go:16.77,21.2 1 1
|
|
||||||
github.com/muety/wakapi/services/alias.go:25.63,27.16 2 1
|
|
||||||
github.com/muety/wakapi/services/alias.go:30.2,30.12 1 1
|
|
||||||
github.com/muety/wakapi/services/alias.go:27.16,29.3 1 1
|
|
||||||
github.com/muety/wakapi/services/alias.go:33.108,34.32 1 1
|
|
||||||
github.com/muety/wakapi/services/alias.go:40.2,41.46 2 1
|
|
||||||
github.com/muety/wakapi/services/alias.go:46.2,46.19 1 1
|
|
||||||
github.com/muety/wakapi/services/alias.go:34.32,35.53 1 1
|
|
||||||
github.com/muety/wakapi/services/alias.go:35.53,37.4 1 1
|
|
||||||
github.com/muety/wakapi/services/alias.go:41.46,42.48 1 1
|
|
||||||
github.com/muety/wakapi/services/alias.go:42.48,44.4 1 1
|
|
||||||
github.com/muety/wakapi/services/alias.go:49.60,50.43 1 1
|
|
||||||
github.com/muety/wakapi/services/alias.go:53.2,53.14 1 1
|
|
||||||
github.com/muety/wakapi/services/alias.go:50.43,52.3 1 1
|
|
||||||
github.com/muety/wakapi/services/heartbeat.go:17.141,23.2 1 0
|
github.com/muety/wakapi/services/heartbeat.go:17.141,23.2 1 0
|
||||||
github.com/muety/wakapi/services/heartbeat.go:25.80,27.2 1 0
|
github.com/muety/wakapi/services/heartbeat.go:25.80,27.2 1 0
|
||||||
github.com/muety/wakapi/services/heartbeat.go:29.111,31.16 2 0
|
github.com/muety/wakapi/services/heartbeat.go:29.111,31.16 2 0
|
||||||
@ -508,7 +577,3 @@ github.com/muety/wakapi/services/heartbeat.go:51.2,51.28 1 0
|
|||||||
github.com/muety/wakapi/services/heartbeat.go:55.2,55.24 1 0
|
github.com/muety/wakapi/services/heartbeat.go:55.2,55.24 1 0
|
||||||
github.com/muety/wakapi/services/heartbeat.go:47.16,49.3 1 0
|
github.com/muety/wakapi/services/heartbeat.go:47.16,49.3 1 0
|
||||||
github.com/muety/wakapi/services/heartbeat.go:51.28,53.3 1 0
|
github.com/muety/wakapi/services/heartbeat.go:51.28,53.3 1 0
|
||||||
github.com/muety/wakapi/services/key_value.go:14.89,19.2 1 0
|
|
||||||
github.com/muety/wakapi/services/key_value.go:21.83,23.2 1 0
|
|
||||||
github.com/muety/wakapi/services/key_value.go:25.72,27.2 1 0
|
|
||||||
github.com/muety/wakapi/services/key_value.go:29.60,31.2 1 0
|
|
||||||
|
4
go.mod
4
go.mod
@ -11,12 +11,14 @@ require (
|
|||||||
github.com/jinzhu/configor v1.2.0
|
github.com/jinzhu/configor v1.2.0
|
||||||
github.com/joho/godotenv v1.3.0
|
github.com/joho/godotenv v1.3.0
|
||||||
github.com/kr/text v0.2.0 // indirect
|
github.com/kr/text v0.2.0 // indirect
|
||||||
|
github.com/markbates/pkger v0.17.1
|
||||||
github.com/mattn/go-sqlite3 v2.0.3+incompatible // indirect
|
github.com/mattn/go-sqlite3 v2.0.3+incompatible // indirect
|
||||||
github.com/niemeyer/pretty v0.0.0-20200227124842-a10e7caefd8e // indirect
|
github.com/niemeyer/pretty v0.0.0-20200227124842-a10e7caefd8e // indirect
|
||||||
github.com/patrickmn/go-cache v2.1.0+incompatible
|
github.com/patrickmn/go-cache v2.1.0+incompatible
|
||||||
github.com/rubenv/sql-migrate v0.0.0-20200402132117-435005d389bc
|
github.com/rubenv/sql-migrate v0.0.0-20200402132117-435005d389bc
|
||||||
github.com/satori/go.uuid v1.2.0
|
github.com/satori/go.uuid v1.2.0
|
||||||
github.com/stretchr/testify v1.6.1
|
github.com/stretchr/testify v1.6.1
|
||||||
|
go.uber.org/atomic v1.6.0
|
||||||
golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9
|
golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9
|
||||||
gopkg.in/check.v1 v1.0.0-20200227125254-8fa46927fb4f // indirect
|
gopkg.in/check.v1 v1.0.0-20200227125254-8fa46927fb4f // indirect
|
||||||
gopkg.in/ini.v1 v1.50.0
|
gopkg.in/ini.v1 v1.50.0
|
||||||
@ -24,5 +26,5 @@ require (
|
|||||||
gorm.io/driver/mysql v1.0.3
|
gorm.io/driver/mysql v1.0.3
|
||||||
gorm.io/driver/postgres v1.0.5
|
gorm.io/driver/postgres v1.0.5
|
||||||
gorm.io/driver/sqlite v1.1.3
|
gorm.io/driver/sqlite v1.1.3
|
||||||
gorm.io/gorm v1.20.5
|
gorm.io/gorm v1.20.11
|
||||||
)
|
)
|
||||||
|
14
go.sum
14
go.sum
@ -81,6 +81,8 @@ github.com/go-stack/stack v1.8.0/go.mod h1:v0f6uXyyMGvRgIKkXu+yp6POWl0qKG85gN/me
|
|||||||
github.com/gobuffalo/envy v1.7.0/go.mod h1:n7DRkBerg/aorDM8kbduw5dN3oXGswK5liaSCx4T5NI=
|
github.com/gobuffalo/envy v1.7.0/go.mod h1:n7DRkBerg/aorDM8kbduw5dN3oXGswK5liaSCx4T5NI=
|
||||||
github.com/gobuffalo/envy v1.7.1 h1:OQl5ys5MBea7OGCdvPbBJWRgnhC/fGona6QKfvFeau8=
|
github.com/gobuffalo/envy v1.7.1 h1:OQl5ys5MBea7OGCdvPbBJWRgnhC/fGona6QKfvFeau8=
|
||||||
github.com/gobuffalo/envy v1.7.1/go.mod h1:FurDp9+EDPE4aIUS3ZLyD+7/9fpx7YRt/ukY6jIHf0w=
|
github.com/gobuffalo/envy v1.7.1/go.mod h1:FurDp9+EDPE4aIUS3ZLyD+7/9fpx7YRt/ukY6jIHf0w=
|
||||||
|
github.com/gobuffalo/here v0.6.0 h1:hYrd0a6gDmWxBM4TnrGw8mQg24iSVoIkHEk7FodQcBI=
|
||||||
|
github.com/gobuffalo/here v0.6.0/go.mod h1:wAG085dHOYqUpf+Ap+WOdrPTp5IYcDAs/x7PLa8Y5fM=
|
||||||
github.com/gobuffalo/logger v1.0.1 h1:ZEgyRGgAm4ZAhAO45YXMs5Fp+bzGLESFewzAVBMKuTg=
|
github.com/gobuffalo/logger v1.0.1 h1:ZEgyRGgAm4ZAhAO45YXMs5Fp+bzGLESFewzAVBMKuTg=
|
||||||
github.com/gobuffalo/logger v1.0.1/go.mod h1:2zbswyIUa45I+c+FLXuWl9zSWEiVuthsk8ze5s8JvPs=
|
github.com/gobuffalo/logger v1.0.1/go.mod h1:2zbswyIUa45I+c+FLXuWl9zSWEiVuthsk8ze5s8JvPs=
|
||||||
github.com/gobuffalo/packd v0.3.0 h1:eMwymTkA1uXsqxS0Tpoop3Lc0u3kTfiMBE6nKtQU4g4=
|
github.com/gobuffalo/packd v0.3.0 h1:eMwymTkA1uXsqxS0Tpoop3Lc0u3kTfiMBE6nKtQU4g4=
|
||||||
@ -94,7 +96,6 @@ github.com/gogo/googleapis v1.1.0/go.mod h1:gf4bu3Q80BeJ6H1S1vYPm8/ELATdvryBaNFG
|
|||||||
github.com/gogo/protobuf v1.1.1/go.mod h1:r8qH/GZQm5c6nD/R0oafs1akxWv10x8SbQlK7atdtwQ=
|
github.com/gogo/protobuf v1.1.1/go.mod h1:r8qH/GZQm5c6nD/R0oafs1akxWv10x8SbQlK7atdtwQ=
|
||||||
github.com/gogo/protobuf v1.2.0/go.mod h1:r8qH/GZQm5c6nD/R0oafs1akxWv10x8SbQlK7atdtwQ=
|
github.com/gogo/protobuf v1.2.0/go.mod h1:r8qH/GZQm5c6nD/R0oafs1akxWv10x8SbQlK7atdtwQ=
|
||||||
github.com/gogo/protobuf v1.2.1/go.mod h1:hp+jE20tsWTFYpLwKvXlhS1hjn+gTNwPg2I6zVXpSg4=
|
github.com/gogo/protobuf v1.2.1/go.mod h1:hp+jE20tsWTFYpLwKvXlhS1hjn+gTNwPg2I6zVXpSg4=
|
||||||
github.com/golang-sql/civil v0.0.0-20190719163853-cb61b32ac6fe h1:lXe2qZdvpiX5WZkZR4hgp4KJVfY3nMkvmwbVkpv1rVY=
|
|
||||||
github.com/golang-sql/civil v0.0.0-20190719163853-cb61b32ac6fe/go.mod h1:8vg3r2VgvsThLBIFL93Qb5yWzgyZWhEmBwUJWevAkK0=
|
github.com/golang-sql/civil v0.0.0-20190719163853-cb61b32ac6fe/go.mod h1:8vg3r2VgvsThLBIFL93Qb5yWzgyZWhEmBwUJWevAkK0=
|
||||||
github.com/golang/glog v0.0.0-20160126235308-23def4e6c14b/go.mod h1:SBH7ygxi8pfUlaOkMMuAQtPIUF8ecWP5IEl/CR7VP2Q=
|
github.com/golang/glog v0.0.0-20160126235308-23def4e6c14b/go.mod h1:SBH7ygxi8pfUlaOkMMuAQtPIUF8ecWP5IEl/CR7VP2Q=
|
||||||
github.com/golang/groupcache v0.0.0-20160516000752-02826c3e7903/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc=
|
github.com/golang/groupcache v0.0.0-20160516000752-02826c3e7903/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc=
|
||||||
@ -243,6 +244,8 @@ github.com/lightstep/lightstep-tracer-common/golang/gogo v0.0.0-20190605223551-b
|
|||||||
github.com/lightstep/lightstep-tracer-go v0.18.1/go.mod h1:jlF1pusYV4pidLvZ+XD0UBX0ZE6WURAspgAczcDHrL4=
|
github.com/lightstep/lightstep-tracer-go v0.18.1/go.mod h1:jlF1pusYV4pidLvZ+XD0UBX0ZE6WURAspgAczcDHrL4=
|
||||||
github.com/lyft/protoc-gen-validate v0.0.13/go.mod h1:XbGvPuh87YZc5TdIa2/I4pLk0QoUACkjt2znoq26NVQ=
|
github.com/lyft/protoc-gen-validate v0.0.13/go.mod h1:XbGvPuh87YZc5TdIa2/I4pLk0QoUACkjt2znoq26NVQ=
|
||||||
github.com/magiconair/properties v1.8.0/go.mod h1:PppfXfuXeibc/6YijjN8zIbojt8czPbwD3XqdrwzmxQ=
|
github.com/magiconair/properties v1.8.0/go.mod h1:PppfXfuXeibc/6YijjN8zIbojt8czPbwD3XqdrwzmxQ=
|
||||||
|
github.com/markbates/pkger v0.17.1 h1:/MKEtWqtc0mZvu9OinB9UzVN9iYCwLWuyUv4Bw+PCno=
|
||||||
|
github.com/markbates/pkger v0.17.1/go.mod h1:0JoVlrol20BSywW79rN3kdFFsE5xYM+rSCQDXbLhiuI=
|
||||||
github.com/mattn/go-colorable v0.0.9/go.mod h1:9vuHe8Xs5qXnSaW/c/ABM9alt+Vo+STaOChaDxuIBZU=
|
github.com/mattn/go-colorable v0.0.9/go.mod h1:9vuHe8Xs5qXnSaW/c/ABM9alt+Vo+STaOChaDxuIBZU=
|
||||||
github.com/mattn/go-colorable v0.1.1/go.mod h1:FuOcm+DKB9mbwrcAfNl7/TZVBZ6rcnceauSikq3lYCQ=
|
github.com/mattn/go-colorable v0.1.1/go.mod h1:FuOcm+DKB9mbwrcAfNl7/TZVBZ6rcnceauSikq3lYCQ=
|
||||||
github.com/mattn/go-colorable v0.1.2/go.mod h1:U0ppj6V5qS13XJ6of8GYAs25YV2eR4EVcfRqFIhoBtE=
|
github.com/mattn/go-colorable v0.1.2/go.mod h1:U0ppj6V5qS13XJ6of8GYAs25YV2eR4EVcfRqFIhoBtE=
|
||||||
@ -387,7 +390,6 @@ github.com/stretchr/objx v0.2.0/go.mod h1:qt09Ya8vawLte6SNmTgCsAVtYtaKzEcn8ATUoH
|
|||||||
github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs=
|
github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs=
|
||||||
github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI=
|
github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI=
|
||||||
github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4=
|
github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4=
|
||||||
github.com/stretchr/testify v1.5.1 h1:nOGnQDM7FYENwehXlg/kFVnos3rEvtKTjRvOWSzb6H4=
|
|
||||||
github.com/stretchr/testify v1.5.1/go.mod h1:5W2xD1RspED5o8YsWQXVCued0rvSQ+mT+I5cxcmMvtA=
|
github.com/stretchr/testify v1.5.1/go.mod h1:5W2xD1RspED5o8YsWQXVCued0rvSQ+mT+I5cxcmMvtA=
|
||||||
github.com/stretchr/testify v1.6.1 h1:hDPOHmpOpP40lSULcqw7IrRb/u7w6RpDC9399XyoNd0=
|
github.com/stretchr/testify v1.6.1 h1:hDPOHmpOpP40lSULcqw7IrRb/u7w6RpDC9399XyoNd0=
|
||||||
github.com/stretchr/testify v1.6.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
|
github.com/stretchr/testify v1.6.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
|
||||||
@ -408,6 +410,7 @@ go.opencensus.io v0.22.2/go.mod h1:yxeiOL68Rb0Xd1ddK5vPZ/oVn4vY4Ynel7k9FzqtOIw=
|
|||||||
go.uber.org/atomic v1.3.2/go.mod h1:gD2HeocX3+yG+ygLZcrzQJaqmWj9AIm7n08wl/qW/PE=
|
go.uber.org/atomic v1.3.2/go.mod h1:gD2HeocX3+yG+ygLZcrzQJaqmWj9AIm7n08wl/qW/PE=
|
||||||
go.uber.org/atomic v1.4.0/go.mod h1:gD2HeocX3+yG+ygLZcrzQJaqmWj9AIm7n08wl/qW/PE=
|
go.uber.org/atomic v1.4.0/go.mod h1:gD2HeocX3+yG+ygLZcrzQJaqmWj9AIm7n08wl/qW/PE=
|
||||||
go.uber.org/atomic v1.5.0/go.mod h1:sABNBOSYdrvTF6hTgEIbc7YasKWGhgEQZyfxyTvoXHQ=
|
go.uber.org/atomic v1.5.0/go.mod h1:sABNBOSYdrvTF6hTgEIbc7YasKWGhgEQZyfxyTvoXHQ=
|
||||||
|
go.uber.org/atomic v1.6.0 h1:Ezj3JGmsOnG1MoRWQkPBsKLe9DwWD9QeXzTRzzldNVk=
|
||||||
go.uber.org/atomic v1.6.0/go.mod h1:sABNBOSYdrvTF6hTgEIbc7YasKWGhgEQZyfxyTvoXHQ=
|
go.uber.org/atomic v1.6.0/go.mod h1:sABNBOSYdrvTF6hTgEIbc7YasKWGhgEQZyfxyTvoXHQ=
|
||||||
go.uber.org/multierr v1.1.0/go.mod h1:wR5kodmAFQ0UK8QlbwjlSNy0Z68gJhDJUG5sjR94q/0=
|
go.uber.org/multierr v1.1.0/go.mod h1:wR5kodmAFQ0UK8QlbwjlSNy0Z68gJhDJUG5sjR94q/0=
|
||||||
go.uber.org/multierr v1.3.0/go.mod h1:VgVr7evmIr6uPjLBxg28wmKNXyqE9akIJ5XnfpiKl+4=
|
go.uber.org/multierr v1.3.0/go.mod h1:VgVr7evmIr6uPjLBxg28wmKNXyqE9akIJ5XnfpiKl+4=
|
||||||
@ -436,6 +439,7 @@ golang.org/x/lint v0.0.0-20181026193005-c67002cb31c3/go.mod h1:UVdnD1Gm6xHRNCYTk
|
|||||||
golang.org/x/lint v0.0.0-20190227174305-5b3e6a55c961/go.mod h1:wehouNa3lNwaWXcvxsM5YxQ5yQlVC4a0KAMCusXpPoU=
|
golang.org/x/lint v0.0.0-20190227174305-5b3e6a55c961/go.mod h1:wehouNa3lNwaWXcvxsM5YxQ5yQlVC4a0KAMCusXpPoU=
|
||||||
golang.org/x/lint v0.0.0-20190301231843-5614ed5bae6f/go.mod h1:UVdnD1Gm6xHRNCYTkRU2/jEulfH38KcIWyp/GAMgvoE=
|
golang.org/x/lint v0.0.0-20190301231843-5614ed5bae6f/go.mod h1:UVdnD1Gm6xHRNCYTkRU2/jEulfH38KcIWyp/GAMgvoE=
|
||||||
golang.org/x/lint v0.0.0-20190313153728-d0100b6bd8b3/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc=
|
golang.org/x/lint v0.0.0-20190313153728-d0100b6bd8b3/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc=
|
||||||
|
golang.org/x/lint v0.0.0-20190930215403-16217165b5de h1:5hukYrvBGR8/eNkX5mdUezrA6JiaEZDtJb9Ei+1LlBs=
|
||||||
golang.org/x/lint v0.0.0-20190930215403-16217165b5de/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc=
|
golang.org/x/lint v0.0.0-20190930215403-16217165b5de/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc=
|
||||||
golang.org/x/mod v0.0.0-20190513183733-4bf6d317e70e/go.mod h1:mXi4GBBbnImb6dmsKGUJ2LatrhH/nqhxcFungHvyanc=
|
golang.org/x/mod v0.0.0-20190513183733-4bf6d317e70e/go.mod h1:mXi4GBBbnImb6dmsKGUJ2LatrhH/nqhxcFungHvyanc=
|
||||||
golang.org/x/mod v0.1.1-0.20191105210325-c90efee705ee/go.mod h1:QqPTAvyqsEbceGzBzNggFXnrqF1CaUcvgkdR5Ot7KZg=
|
golang.org/x/mod v0.1.1-0.20191105210325-c90efee705ee/go.mod h1:QqPTAvyqsEbceGzBzNggFXnrqF1CaUcvgkdR5Ot7KZg=
|
||||||
@ -509,6 +513,7 @@ golang.org/x/tools v0.0.0-20190823170909-c4a336ef6a2f/go.mod h1:b+2E5dAYhXwXZwtn
|
|||||||
golang.org/x/tools v0.0.0-20191004055002-72853e10c5a3/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
|
golang.org/x/tools v0.0.0-20191004055002-72853e10c5a3/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
|
||||||
golang.org/x/tools v0.0.0-20191029041327-9cc4af7d6b2c/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
|
golang.org/x/tools v0.0.0-20191029041327-9cc4af7d6b2c/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
|
||||||
golang.org/x/tools v0.0.0-20191029190741-b9c20aec41a5/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
|
golang.org/x/tools v0.0.0-20191029190741-b9c20aec41a5/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
|
||||||
|
golang.org/x/tools v0.0.0-20200103221440-774c71fcf114 h1:DnSr2mCsxyCE6ZgIkmcWUQY2R5cH/6wL7eIxEmQOMSE=
|
||||||
golang.org/x/tools v0.0.0-20200103221440-774c71fcf114/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28=
|
golang.org/x/tools v0.0.0-20200103221440-774c71fcf114/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28=
|
||||||
golang.org/x/xerrors v0.0.0-20190410155217-1f06c39b4373/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
|
golang.org/x/xerrors v0.0.0-20190410155217-1f06c39b4373/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
|
||||||
golang.org/x/xerrors v0.0.0-20190513163551-3ee3066db522/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
|
golang.org/x/xerrors v0.0.0-20190513163551-3ee3066db522/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
|
||||||
@ -556,6 +561,7 @@ gopkg.in/yaml.v2 v2.0.0-20170812160011-eb3733d160e7/go.mod h1:JAlM8MvJe8wmxCU4Bl
|
|||||||
gopkg.in/yaml.v2 v2.2.1/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
|
gopkg.in/yaml.v2 v2.2.1/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
|
||||||
gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
|
gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
|
||||||
gopkg.in/yaml.v2 v2.2.5/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
|
gopkg.in/yaml.v2 v2.2.5/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
|
||||||
|
gopkg.in/yaml.v2 v2.2.7/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
|
||||||
gopkg.in/yaml.v2 v2.2.8 h1:obN1ZagJSUGI0Ek/LBmuj4SNLPfIny3KsKFopxRdj10=
|
gopkg.in/yaml.v2 v2.2.8 h1:obN1ZagJSUGI0Ek/LBmuj4SNLPfIny3KsKFopxRdj10=
|
||||||
gopkg.in/yaml.v2 v2.2.8/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
|
gopkg.in/yaml.v2 v2.2.8/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
|
||||||
gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c h1:dUUwHk2QECo/6vqA44rthZ8ie2QXMNeKRTHCNY2nXvo=
|
gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c h1:dUUwHk2QECo/6vqA44rthZ8ie2QXMNeKRTHCNY2nXvo=
|
||||||
@ -568,8 +574,8 @@ gorm.io/driver/sqlite v1.1.3 h1:BYfdVuZB5He/u9dt4qDpZqiqDJ6KhPqs5QUqsr/Eeuc=
|
|||||||
gorm.io/driver/sqlite v1.1.3/go.mod h1:AKDgRWk8lcSQSw+9kxCJnX/yySj8G3rdwYlU57cB45c=
|
gorm.io/driver/sqlite v1.1.3/go.mod h1:AKDgRWk8lcSQSw+9kxCJnX/yySj8G3rdwYlU57cB45c=
|
||||||
gorm.io/gorm v1.20.1/go.mod h1:0HFTzE/SqkGTzK6TlDPPQbAYCluiVvhzoA1+aVyzenw=
|
gorm.io/gorm v1.20.1/go.mod h1:0HFTzE/SqkGTzK6TlDPPQbAYCluiVvhzoA1+aVyzenw=
|
||||||
gorm.io/gorm v1.20.4/go.mod h1:0HFTzE/SqkGTzK6TlDPPQbAYCluiVvhzoA1+aVyzenw=
|
gorm.io/gorm v1.20.4/go.mod h1:0HFTzE/SqkGTzK6TlDPPQbAYCluiVvhzoA1+aVyzenw=
|
||||||
gorm.io/gorm v1.20.5 h1:g3tpSF9kggASzReK+Z3dYei1IJODLqNUbOjSuCczY8g=
|
gorm.io/gorm v1.20.11 h1:jYHQ0LLUViV85V8dM1TP9VBBkfzKTnuTXDjYObkI6yc=
|
||||||
gorm.io/gorm v1.20.5/go.mod h1:0HFTzE/SqkGTzK6TlDPPQbAYCluiVvhzoA1+aVyzenw=
|
gorm.io/gorm v1.20.11/go.mod h1:0HFTzE/SqkGTzK6TlDPPQbAYCluiVvhzoA1+aVyzenw=
|
||||||
honnef.co/go/tools v0.0.0-20180728063816-88497007e858/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4=
|
honnef.co/go/tools v0.0.0-20180728063816-88497007e858/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4=
|
||||||
honnef.co/go/tools v0.0.0-20190102054323-c2f93a96b099/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4=
|
honnef.co/go/tools v0.0.0-20190102054323-c2f93a96b099/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4=
|
||||||
honnef.co/go/tools v0.0.0-20190523083050-ea95bdfd59fc/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4=
|
honnef.co/go/tools v0.0.0-20190523083050-ea95bdfd59fc/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4=
|
||||||
|
24
main.go
24
main.go
@ -1,8 +1,11 @@
|
|||||||
package main
|
package main
|
||||||
|
|
||||||
|
//go:generate $GOPATH/bin/pkger
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"fmt"
|
"fmt"
|
||||||
"github.com/gorilla/handlers"
|
"github.com/gorilla/handlers"
|
||||||
|
"github.com/markbates/pkger"
|
||||||
conf "github.com/muety/wakapi/config"
|
conf "github.com/muety/wakapi/config"
|
||||||
"github.com/muety/wakapi/migrations/common"
|
"github.com/muety/wakapi/migrations/common"
|
||||||
"github.com/muety/wakapi/repositories"
|
"github.com/muety/wakapi/repositories"
|
||||||
@ -13,6 +16,7 @@ import (
|
|||||||
|
|
||||||
"github.com/gorilla/mux"
|
"github.com/gorilla/mux"
|
||||||
"github.com/muety/wakapi/middlewares"
|
"github.com/muety/wakapi/middlewares"
|
||||||
|
customMiddleware "github.com/muety/wakapi/middlewares/custom"
|
||||||
"github.com/muety/wakapi/routes"
|
"github.com/muety/wakapi/routes"
|
||||||
shieldsV1Routes "github.com/muety/wakapi/routes/compat/shields/v1"
|
shieldsV1Routes "github.com/muety/wakapi/routes/compat/shields/v1"
|
||||||
wtV1Routes "github.com/muety/wakapi/routes/compat/wakatime/v1"
|
wtV1Routes "github.com/muety/wakapi/routes/compat/wakatime/v1"
|
||||||
@ -45,6 +49,7 @@ var (
|
|||||||
summaryService services.ISummaryService
|
summaryService services.ISummaryService
|
||||||
aggregationService services.IAggregationService
|
aggregationService services.IAggregationService
|
||||||
keyValueService services.IKeyValueService
|
keyValueService services.IKeyValueService
|
||||||
|
miscService services.IMiscService
|
||||||
)
|
)
|
||||||
|
|
||||||
// TODO: Refactor entire project to be structured after business domains
|
// TODO: Refactor entire project to be structured after business domains
|
||||||
@ -97,9 +102,11 @@ func main() {
|
|||||||
summaryService = services.NewSummaryService(summaryRepository, heartbeatService, aliasService)
|
summaryService = services.NewSummaryService(summaryRepository, heartbeatService, aliasService)
|
||||||
aggregationService = services.NewAggregationService(userService, summaryService, heartbeatService)
|
aggregationService = services.NewAggregationService(userService, summaryService, heartbeatService)
|
||||||
keyValueService = services.NewKeyValueService(keyValueRepository)
|
keyValueService = services.NewKeyValueService(keyValueRepository)
|
||||||
|
miscService = services.NewMiscService(userService, summaryService, keyValueService)
|
||||||
|
|
||||||
// Aggregate heartbeats to summaries and persist them
|
// Schedule background tasks
|
||||||
go aggregationService.Schedule()
|
go aggregationService.Schedule()
|
||||||
|
go miscService.ScheduleCountTotalTime()
|
||||||
|
|
||||||
// TODO: move endpoint registration to the respective routes files
|
// TODO: move endpoint registration to the respective routes files
|
||||||
|
|
||||||
@ -109,8 +116,8 @@ func main() {
|
|||||||
summaryHandler := routes.NewSummaryHandler(summaryService)
|
summaryHandler := routes.NewSummaryHandler(summaryService)
|
||||||
healthHandler := routes.NewHealthHandler(db)
|
healthHandler := routes.NewHealthHandler(db)
|
||||||
heartbeatHandler := routes.NewHeartbeatHandler(heartbeatService, languageMappingService)
|
heartbeatHandler := routes.NewHeartbeatHandler(heartbeatService, languageMappingService)
|
||||||
settingsHandler := routes.NewSettingsHandler(userService, summaryService, aggregationService, languageMappingService)
|
settingsHandler := routes.NewSettingsHandler(userService, summaryService, aliasService, aggregationService, languageMappingService)
|
||||||
homeHandler := routes.NewHomeHandler()
|
homeHandler := routes.NewHomeHandler(keyValueService)
|
||||||
loginHandler := routes.NewLoginHandler(userService)
|
loginHandler := routes.NewLoginHandler(userService)
|
||||||
imprintHandler := routes.NewImprintHandler(keyValueService)
|
imprintHandler := routes.NewImprintHandler(keyValueService)
|
||||||
wakatimeV1AllHandler := wtV1Routes.NewAllTimeHandler(summaryService)
|
wakatimeV1AllHandler := wtV1Routes.NewAllTimeHandler(summaryService)
|
||||||
@ -135,6 +142,7 @@ func main() {
|
|||||||
userService,
|
userService,
|
||||||
[]string{"/api/health", "/api/compat/shields/v1"},
|
[]string{"/api/health", "/api/compat/shields/v1"},
|
||||||
).Handler
|
).Handler
|
||||||
|
wakatimeRelayMiddleware := customMiddleware.NewWakatimeRelayMiddleware().Handler
|
||||||
|
|
||||||
// Router configs
|
// Router configs
|
||||||
router.Use(loggingMiddleware, recoveryMiddleware)
|
router.Use(loggingMiddleware, recoveryMiddleware)
|
||||||
@ -157,17 +165,23 @@ func main() {
|
|||||||
// Settings Routes
|
// Settings Routes
|
||||||
settingsRouter.Methods(http.MethodGet).HandlerFunc(settingsHandler.GetIndex)
|
settingsRouter.Methods(http.MethodGet).HandlerFunc(settingsHandler.GetIndex)
|
||||||
settingsRouter.Path("/credentials").Methods(http.MethodPost).HandlerFunc(settingsHandler.PostCredentials)
|
settingsRouter.Path("/credentials").Methods(http.MethodPost).HandlerFunc(settingsHandler.PostCredentials)
|
||||||
|
settingsRouter.Path("/aliases").Methods(http.MethodPost).HandlerFunc(settingsHandler.PostAlias)
|
||||||
|
settingsRouter.Path("/aliases/delete").Methods(http.MethodPost).HandlerFunc(settingsHandler.DeleteAlias)
|
||||||
settingsRouter.Path("/language_mappings").Methods(http.MethodPost).HandlerFunc(settingsHandler.PostLanguageMapping)
|
settingsRouter.Path("/language_mappings").Methods(http.MethodPost).HandlerFunc(settingsHandler.PostLanguageMapping)
|
||||||
settingsRouter.Path("/language_mappings/delete").Methods(http.MethodPost).HandlerFunc(settingsHandler.DeleteLanguageMapping)
|
settingsRouter.Path("/language_mappings/delete").Methods(http.MethodPost).HandlerFunc(settingsHandler.DeleteLanguageMapping)
|
||||||
settingsRouter.Path("/reset").Methods(http.MethodPost).HandlerFunc(settingsHandler.PostResetApiKey)
|
settingsRouter.Path("/reset").Methods(http.MethodPost).HandlerFunc(settingsHandler.PostResetApiKey)
|
||||||
settingsRouter.Path("/badges").Methods(http.MethodPost).HandlerFunc(settingsHandler.PostToggleBadges)
|
settingsRouter.Path("/badges").Methods(http.MethodPost).HandlerFunc(settingsHandler.PostToggleBadges)
|
||||||
|
settingsRouter.Path("/wakatime_integration").Methods(http.MethodPost).HandlerFunc(settingsHandler.PostSetWakatimeApiKey)
|
||||||
settingsRouter.Path("/regenerate").Methods(http.MethodPost).HandlerFunc(settingsHandler.PostRegenerateSummaries)
|
settingsRouter.Path("/regenerate").Methods(http.MethodPost).HandlerFunc(settingsHandler.PostRegenerateSummaries)
|
||||||
|
|
||||||
// API Routes
|
// API Routes
|
||||||
apiRouter.Path("/heartbeat").Methods(http.MethodPost).HandlerFunc(heartbeatHandler.ApiPost)
|
|
||||||
apiRouter.Path("/summary").Methods(http.MethodGet).HandlerFunc(summaryHandler.ApiGet)
|
apiRouter.Path("/summary").Methods(http.MethodGet).HandlerFunc(summaryHandler.ApiGet)
|
||||||
apiRouter.Path("/health").Methods(http.MethodGet).HandlerFunc(healthHandler.ApiGet)
|
apiRouter.Path("/health").Methods(http.MethodGet).HandlerFunc(healthHandler.ApiGet)
|
||||||
|
|
||||||
|
heartbeatsApiRouter := apiRouter.Path("/heartbeat").Methods(http.MethodPost).Subrouter()
|
||||||
|
heartbeatsApiRouter.Use(wakatimeRelayMiddleware)
|
||||||
|
heartbeatsApiRouter.Path("").HandlerFunc(heartbeatHandler.ApiPost)
|
||||||
|
|
||||||
// Wakatime compat V1 API Routes
|
// Wakatime compat V1 API Routes
|
||||||
wakatimeV1Router.Path("/users/{user}/all_time_since_today").Methods(http.MethodGet).HandlerFunc(wakatimeV1AllHandler.ApiGet)
|
wakatimeV1Router.Path("/users/{user}/all_time_since_today").Methods(http.MethodGet).HandlerFunc(wakatimeV1AllHandler.ApiGet)
|
||||||
wakatimeV1Router.Path("/users/{user}/summaries").Methods(http.MethodGet).HandlerFunc(wakatimeV1SummariesHandler.ApiGet)
|
wakatimeV1Router.Path("/users/{user}/summaries").Methods(http.MethodGet).HandlerFunc(wakatimeV1SummariesHandler.ApiGet)
|
||||||
@ -176,7 +190,7 @@ func main() {
|
|||||||
shieldsV1Router.PathPrefix("/{user}").Methods(http.MethodGet).HandlerFunc(shieldV1BadgeHandler.ApiGet)
|
shieldsV1Router.PathPrefix("/{user}").Methods(http.MethodGet).HandlerFunc(shieldV1BadgeHandler.ApiGet)
|
||||||
|
|
||||||
// Static Routes
|
// Static Routes
|
||||||
router.PathPrefix("/assets").Handler(http.FileServer(http.Dir("./static")))
|
router.PathPrefix("/assets").Handler(http.FileServer(pkger.Dir("/static")))
|
||||||
|
|
||||||
// Listen HTTP
|
// Listen HTTP
|
||||||
listen(router)
|
listen(router)
|
||||||
|
@ -5,21 +5,16 @@ import (
|
|||||||
"errors"
|
"errors"
|
||||||
"fmt"
|
"fmt"
|
||||||
conf "github.com/muety/wakapi/config"
|
conf "github.com/muety/wakapi/config"
|
||||||
|
"github.com/muety/wakapi/models"
|
||||||
|
"github.com/muety/wakapi/services"
|
||||||
"github.com/muety/wakapi/utils"
|
"github.com/muety/wakapi/utils"
|
||||||
"log"
|
"log"
|
||||||
"net/http"
|
"net/http"
|
||||||
"strings"
|
"strings"
|
||||||
"time"
|
|
||||||
|
|
||||||
"github.com/patrickmn/go-cache"
|
|
||||||
|
|
||||||
"github.com/muety/wakapi/models"
|
|
||||||
"github.com/muety/wakapi/services"
|
|
||||||
)
|
)
|
||||||
|
|
||||||
type AuthenticateMiddleware struct {
|
type AuthenticateMiddleware struct {
|
||||||
config *conf.Config
|
config *conf.Config
|
||||||
cache *cache.Cache
|
|
||||||
userSrvc services.IUserService
|
userSrvc services.IUserService
|
||||||
whitelistPaths []string
|
whitelistPaths []string
|
||||||
}
|
}
|
||||||
@ -28,7 +23,6 @@ func NewAuthenticateMiddleware(userService services.IUserService, whitelistPaths
|
|||||||
return &AuthenticateMiddleware{
|
return &AuthenticateMiddleware{
|
||||||
config: conf.Get(),
|
config: conf.Get(),
|
||||||
userSrvc: userService,
|
userSrvc: userService,
|
||||||
cache: cache.New(1*time.Hour, 2*time.Hour),
|
|
||||||
whitelistPaths: whitelistPaths,
|
whitelistPaths: whitelistPaths,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -64,8 +58,6 @@ func (m *AuthenticateMiddleware) ServeHTTP(w http.ResponseWriter, r *http.Reques
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
m.cache.Set(user.ID, user, cache.DefaultExpiration)
|
|
||||||
|
|
||||||
ctx := context.WithValue(r.Context(), models.UserKey, user)
|
ctx := context.WithValue(r.Context(), models.UserKey, user)
|
||||||
next(w, r.WithContext(ctx))
|
next(w, r.WithContext(ctx))
|
||||||
}
|
}
|
||||||
@ -78,15 +70,10 @@ func (m *AuthenticateMiddleware) tryGetUserByApiKey(r *http.Request) (*models.Us
|
|||||||
|
|
||||||
var user *models.User
|
var user *models.User
|
||||||
userKey := strings.TrimSpace(key)
|
userKey := strings.TrimSpace(key)
|
||||||
cachedUser, ok := m.cache.Get(userKey)
|
|
||||||
if !ok {
|
|
||||||
user, err = m.userSrvc.GetUserByKey(userKey)
|
user, err = m.userSrvc.GetUserByKey(userKey)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
} else {
|
|
||||||
user = cachedUser.(*models.User)
|
|
||||||
}
|
|
||||||
return user, nil
|
return user, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -96,12 +83,6 @@ func (m *AuthenticateMiddleware) tryGetUserByCookie(r *http.Request) (*models.Us
|
|||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
|
||||||
cachedUser, ok := m.cache.Get(login.Username)
|
|
||||||
|
|
||||||
if ok {
|
|
||||||
return cachedUser.(*models.User), nil
|
|
||||||
}
|
|
||||||
|
|
||||||
user, err := m.userSrvc.GetUserById(login.Username)
|
user, err := m.userSrvc.GetUserById(login.Username)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
|
@ -6,7 +6,6 @@ import (
|
|||||||
"github.com/muety/wakapi/mocks"
|
"github.com/muety/wakapi/mocks"
|
||||||
"github.com/muety/wakapi/models"
|
"github.com/muety/wakapi/models"
|
||||||
"github.com/stretchr/testify/assert"
|
"github.com/stretchr/testify/assert"
|
||||||
"github.com/stretchr/testify/mock"
|
|
||||||
"net/http"
|
"net/http"
|
||||||
"testing"
|
"testing"
|
||||||
)
|
)
|
||||||
@ -33,30 +32,6 @@ func TestAuthenticateMiddleware_tryGetUserByApiKey_Success(t *testing.T) {
|
|||||||
assert.Equal(t, testUser, result)
|
assert.Equal(t, testUser, result)
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestAuthenticateMiddleware_tryGetUserByApiKey_GetFromCache(t *testing.T) {
|
|
||||||
testApiKey := "z5uig69cn9ut93n"
|
|
||||||
testToken := base64.StdEncoding.EncodeToString([]byte(testApiKey))
|
|
||||||
testUser := &models.User{ApiKey: testApiKey}
|
|
||||||
|
|
||||||
mockRequest := &http.Request{
|
|
||||||
Header: http.Header{
|
|
||||||
"Authorization": []string{fmt.Sprintf("Basic %s", testToken)},
|
|
||||||
},
|
|
||||||
}
|
|
||||||
|
|
||||||
userServiceMock := new(mocks.UserServiceMock)
|
|
||||||
userServiceMock.On("GetUserByKey", testApiKey).Return(testUser, nil)
|
|
||||||
|
|
||||||
sut := NewAuthenticateMiddleware(userServiceMock, []string{})
|
|
||||||
sut.cache.SetDefault(testApiKey, testUser)
|
|
||||||
|
|
||||||
result, err := sut.tryGetUserByApiKey(mockRequest)
|
|
||||||
|
|
||||||
assert.Nil(t, err)
|
|
||||||
assert.Equal(t, testUser, result)
|
|
||||||
userServiceMock.AssertNotCalled(t, "GetUserByKey", mock.Anything)
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestAuthenticateMiddleware_tryGetUserByApiKey_InvalidHeader(t *testing.T) {
|
func TestAuthenticateMiddleware_tryGetUserByApiKey_InvalidHeader(t *testing.T) {
|
||||||
testApiKey := "z5uig69cn9ut93n"
|
testApiKey := "z5uig69cn9ut93n"
|
||||||
testToken := base64.StdEncoding.EncodeToString([]byte(testApiKey))
|
testToken := base64.StdEncoding.EncodeToString([]byte(testApiKey))
|
||||||
|
97
middlewares/custom/wakatime.go
Normal file
97
middlewares/custom/wakatime.go
Normal file
@ -0,0 +1,97 @@
|
|||||||
|
package relay
|
||||||
|
|
||||||
|
import (
|
||||||
|
"bytes"
|
||||||
|
"encoding/base64"
|
||||||
|
"fmt"
|
||||||
|
"github.com/muety/wakapi/config"
|
||||||
|
"github.com/muety/wakapi/models"
|
||||||
|
"io"
|
||||||
|
"io/ioutil"
|
||||||
|
"log"
|
||||||
|
"net/http"
|
||||||
|
"time"
|
||||||
|
)
|
||||||
|
|
||||||
|
const (
|
||||||
|
WakatimeApiUrl = "https://wakatime.com/api/v1"
|
||||||
|
WakatimeApiHeartbeatsEndpoint = "/users/current/heartbeats.bulk"
|
||||||
|
)
|
||||||
|
|
||||||
|
/* Middleware to conditionally relay heartbeats to Wakatime */
|
||||||
|
type WakatimeRelayMiddleware struct {
|
||||||
|
httpClient *http.Client
|
||||||
|
}
|
||||||
|
|
||||||
|
func NewWakatimeRelayMiddleware() *WakatimeRelayMiddleware {
|
||||||
|
return &WakatimeRelayMiddleware{
|
||||||
|
httpClient: &http.Client{
|
||||||
|
Timeout: 10 * time.Second,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (m *WakatimeRelayMiddleware) Handler(h http.Handler) http.Handler {
|
||||||
|
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||||
|
m.ServeHTTP(w, r, h.ServeHTTP)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
func (m *WakatimeRelayMiddleware) ServeHTTP(w http.ResponseWriter, r *http.Request, next http.HandlerFunc) {
|
||||||
|
defer next(w, r)
|
||||||
|
|
||||||
|
if r.Method != http.MethodPost {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
user := r.Context().Value(models.UserKey).(*models.User)
|
||||||
|
if user == nil || user.WakatimeApiKey == "" {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
body, _ := ioutil.ReadAll(r.Body)
|
||||||
|
r.Body.Close()
|
||||||
|
r.Body = ioutil.NopCloser(bytes.NewBuffer(body))
|
||||||
|
|
||||||
|
headers := http.Header{
|
||||||
|
"X-Machine-Name": r.Header.Values("X-Machine-Name"),
|
||||||
|
"Content-Type": r.Header.Values("Content-Type"),
|
||||||
|
"Accept": r.Header.Values("Accept"),
|
||||||
|
"User-Agent": r.Header.Values("User-Agent"),
|
||||||
|
"X-Origin": []string{
|
||||||
|
fmt.Sprintf("wakapi v%s", config.Get().Version),
|
||||||
|
},
|
||||||
|
"Authorization": []string{
|
||||||
|
fmt.Sprintf("Basic %s", base64.StdEncoding.EncodeToString([]byte(user.WakatimeApiKey))),
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
go m.send(
|
||||||
|
http.MethodPost,
|
||||||
|
WakatimeApiUrl+WakatimeApiHeartbeatsEndpoint,
|
||||||
|
bytes.NewReader(body),
|
||||||
|
headers,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (m *WakatimeRelayMiddleware) send(method, url string, body io.Reader, headers http.Header) {
|
||||||
|
request, err := http.NewRequest(method, url, body)
|
||||||
|
if err != nil {
|
||||||
|
log.Printf("error constructing relayed request – %v\n", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
for k, v := range headers {
|
||||||
|
for _, h := range v {
|
||||||
|
request.Header.Set(k, h)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
response, err := m.httpClient.Do(request)
|
||||||
|
if err != nil {
|
||||||
|
log.Printf("error executing relayed request – %v\n", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
if response.StatusCode < 200 || response.StatusCode >= 300 {
|
||||||
|
log.Printf("failed to relay request, got status %d\n", response.StatusCode)
|
||||||
|
}
|
||||||
|
}
|
@ -13,3 +13,33 @@ func (m *AliasRepositoryMock) GetByUser(s string) ([]*models.Alias, error) {
|
|||||||
args := m.Called(s)
|
args := m.Called(s)
|
||||||
return args.Get(0).([]*models.Alias), args.Error(1)
|
return args.Get(0).([]*models.Alias), args.Error(1)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (m *AliasRepositoryMock) GetByUserAndKey(s string, s2 string) ([]*models.Alias, error) {
|
||||||
|
args := m.Called(s, s2)
|
||||||
|
return args.Get(0).([]*models.Alias), args.Error(1)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (m *AliasRepositoryMock) GetByUserAndKeyAndType(s string, s2 string, u uint8) ([]*models.Alias, error) {
|
||||||
|
args := m.Called(s, s2, u)
|
||||||
|
return args.Get(0).([]*models.Alias), args.Error(1)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (m *AliasRepositoryMock) GetByUserAndTypeAndValue(s string, u uint8, s2 string) (*models.Alias, error) {
|
||||||
|
args := m.Called(s, u, s2)
|
||||||
|
return args.Get(0).(*models.Alias), args.Error(1)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (m *AliasRepositoryMock) Insert(s *models.Alias) (*models.Alias, error) {
|
||||||
|
args := m.Called(s)
|
||||||
|
return args.Get(0).(*models.Alias), args.Error(1)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (m *AliasRepositoryMock) Delete(u uint) error {
|
||||||
|
args := m.Called(u)
|
||||||
|
return args.Error(0)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (m *AliasRepositoryMock) DeleteBatch(u []uint) error {
|
||||||
|
args := m.Called(u)
|
||||||
|
return args.Error(0)
|
||||||
|
}
|
||||||
|
@ -1,6 +1,7 @@
|
|||||||
package mocks
|
package mocks
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"github.com/muety/wakapi/models"
|
||||||
"github.com/stretchr/testify/mock"
|
"github.com/stretchr/testify/mock"
|
||||||
)
|
)
|
||||||
|
|
||||||
@ -8,7 +9,12 @@ type AliasServiceMock struct {
|
|||||||
mock.Mock
|
mock.Mock
|
||||||
}
|
}
|
||||||
|
|
||||||
func (m *AliasServiceMock) LoadUserAliases(s string) error {
|
func (m *AliasServiceMock) IsInitialized(s string) bool {
|
||||||
|
args := m.Called(s)
|
||||||
|
return args.Bool(0)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (m *AliasServiceMock) InitializeUser(s string) error {
|
||||||
args := m.Called(s)
|
args := m.Called(s)
|
||||||
return args.Error(0)
|
return args.Error(0)
|
||||||
}
|
}
|
||||||
@ -18,7 +24,27 @@ func (m *AliasServiceMock) GetAliasOrDefault(s string, u uint8, s2 string) (stri
|
|||||||
return args.String(0), args.Error(1)
|
return args.String(0), args.Error(1)
|
||||||
}
|
}
|
||||||
|
|
||||||
func (m *AliasServiceMock) IsInitialized(s string) bool {
|
func (m *AliasServiceMock) GetByUser(s string) ([]*models.Alias, error) {
|
||||||
args := m.Called(s)
|
args := m.Called(s)
|
||||||
return args.Bool(0)
|
return args.Get(0).([]*models.Alias), args.Error(1)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (m *AliasServiceMock) GetByUserAndKeyAndType(s string, s2 string, u uint8) ([]*models.Alias, error) {
|
||||||
|
args := m.Called(s, s2, u)
|
||||||
|
return args.Get(0).([]*models.Alias), args.Error(1)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (m *AliasServiceMock) Create(a *models.Alias) (*models.Alias, error) {
|
||||||
|
args := m.Called(a)
|
||||||
|
return args.Get(0).(*models.Alias), args.Error(1)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (m *AliasServiceMock) Delete(s *models.Alias) error {
|
||||||
|
args := m.Called(s)
|
||||||
|
return args.Error(0)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (m *AliasServiceMock) DeleteMulti(a []*models.Alias) error {
|
||||||
|
args := m.Called(a)
|
||||||
|
return args.Error(0)
|
||||||
}
|
}
|
||||||
|
@ -44,6 +44,11 @@ func (m *UserServiceMock) ToggleBadges(user *models.User) (*models.User, error)
|
|||||||
return args.Get(0).(*models.User), args.Error(1)
|
return args.Get(0).(*models.User), args.Error(1)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (m *UserServiceMock) SetWakatimeApiKey(user *models.User, s string) (*models.User, error) {
|
||||||
|
args := m.Called(user, s)
|
||||||
|
return args.Get(0).(*models.User), args.Error(1)
|
||||||
|
}
|
||||||
|
|
||||||
func (m *UserServiceMock) MigrateMd5Password(user *models.User, login *models.Login) (*models.User, error) {
|
func (m *UserServiceMock) MigrateMd5Password(user *models.User, login *models.Login) (*models.User, error) {
|
||||||
args := m.Called(user, login)
|
args := m.Called(user, login)
|
||||||
return args.Get(0).(*models.User), args.Error(1)
|
return args.Get(0).(*models.User), args.Error(1)
|
||||||
|
@ -8,3 +8,16 @@ type Alias struct {
|
|||||||
Key string `gorm:"not null; index:idx_alias_type_key"`
|
Key string `gorm:"not null; index:idx_alias_type_key"`
|
||||||
Value string `gorm:"not null"`
|
Value string `gorm:"not null"`
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (a *Alias) IsValid() bool {
|
||||||
|
return a.Key != "" && a.Value != "" && a.validateType()
|
||||||
|
}
|
||||||
|
|
||||||
|
func (a *Alias) validateType() bool {
|
||||||
|
for _, t := range SummaryTypes() {
|
||||||
|
if a.Type == t {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
@ -7,6 +7,7 @@ type User struct {
|
|||||||
CreatedAt CustomTime `gorm:"type:timestamp; default:CURRENT_TIMESTAMP"`
|
CreatedAt CustomTime `gorm:"type:timestamp; default:CURRENT_TIMESTAMP"`
|
||||||
LastLoggedInAt CustomTime `gorm:"type:timestamp; default:CURRENT_TIMESTAMP"`
|
LastLoggedInAt CustomTime `gorm:"type:timestamp; default:CURRENT_TIMESTAMP"`
|
||||||
BadgesEnabled bool `json:"-" gorm:"default:false; type:bool"`
|
BadgesEnabled bool `json:"-" gorm:"default:false; type:bool"`
|
||||||
|
WakatimeApiKey string `json:"-"`
|
||||||
}
|
}
|
||||||
|
|
||||||
type Login struct {
|
type Login struct {
|
||||||
@ -43,7 +44,7 @@ func (s *Signup) IsValid() bool {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func validateUsername(username string) bool {
|
func validateUsername(username string) bool {
|
||||||
return len(username) >= 3 && username != "current"
|
return len(username) >= 1 && username != "current"
|
||||||
}
|
}
|
||||||
|
|
||||||
func validatePassword(password string) bool {
|
func validatePassword(password string) bool {
|
||||||
|
@ -3,6 +3,8 @@ package view
|
|||||||
type HomeViewModel struct {
|
type HomeViewModel struct {
|
||||||
Success string
|
Success string
|
||||||
Error string
|
Error string
|
||||||
|
TotalHours int
|
||||||
|
TotalUsers int
|
||||||
}
|
}
|
||||||
|
|
||||||
func (s *HomeViewModel) WithSuccess(m string) *HomeViewModel {
|
func (s *HomeViewModel) WithSuccess(m string) *HomeViewModel {
|
||||||
|
@ -5,10 +5,17 @@ import "github.com/muety/wakapi/models"
|
|||||||
type SettingsViewModel struct {
|
type SettingsViewModel struct {
|
||||||
User *models.User
|
User *models.User
|
||||||
LanguageMappings []*models.LanguageMapping
|
LanguageMappings []*models.LanguageMapping
|
||||||
|
Aliases []*SettingsVMCombinedAlias
|
||||||
Success string
|
Success string
|
||||||
Error string
|
Error string
|
||||||
}
|
}
|
||||||
|
|
||||||
|
type SettingsVMCombinedAlias struct {
|
||||||
|
Key string
|
||||||
|
Type uint8
|
||||||
|
Values []string
|
||||||
|
}
|
||||||
|
|
||||||
func (s *SettingsViewModel) WithSuccess(m string) *SettingsViewModel {
|
func (s *SettingsViewModel) WithSuccess(m string) *SettingsViewModel {
|
||||||
s.Success = m
|
s.Success = m
|
||||||
return s
|
return s
|
||||||
|
@ -1,6 +1,7 @@
|
|||||||
package repositories
|
package repositories
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"errors"
|
||||||
"github.com/muety/wakapi/models"
|
"github.com/muety/wakapi/models"
|
||||||
"gorm.io/gorm"
|
"gorm.io/gorm"
|
||||||
)
|
)
|
||||||
@ -22,3 +23,67 @@ func (r *AliasRepository) GetByUser(userId string) ([]*models.Alias, error) {
|
|||||||
}
|
}
|
||||||
return aliases, nil
|
return aliases, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (r *AliasRepository) GetByUserAndKey(userId, key string) ([]*models.Alias, error) {
|
||||||
|
var aliases []*models.Alias
|
||||||
|
if err := r.db.
|
||||||
|
Where(&models.Alias{
|
||||||
|
UserID: userId,
|
||||||
|
Key: key,
|
||||||
|
}).
|
||||||
|
Find(&aliases).Error; err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
return aliases, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (r *AliasRepository) GetByUserAndKeyAndType(userId, key string, summaryType uint8) ([]*models.Alias, error) {
|
||||||
|
var aliases []*models.Alias
|
||||||
|
if err := r.db.
|
||||||
|
Where(&models.Alias{
|
||||||
|
UserID: userId,
|
||||||
|
Key: key,
|
||||||
|
Type: summaryType,
|
||||||
|
}).
|
||||||
|
Find(&aliases).Error; err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
return aliases, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (r *AliasRepository) GetByUserAndTypeAndValue(userId string, summaryType uint8, value string) (*models.Alias, error) {
|
||||||
|
alias := &models.Alias{}
|
||||||
|
if err := r.db.
|
||||||
|
Where(&models.Alias{
|
||||||
|
UserID: userId,
|
||||||
|
Type: summaryType,
|
||||||
|
Value: value,
|
||||||
|
}).
|
||||||
|
First(alias).Error; err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
return alias, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (r *AliasRepository) Insert(alias *models.Alias) (*models.Alias, error) {
|
||||||
|
if !alias.IsValid() {
|
||||||
|
return nil, errors.New("invalid alias")
|
||||||
|
}
|
||||||
|
result := r.db.Create(alias)
|
||||||
|
if err := result.Error; err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
return alias, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (r *AliasRepository) Delete(id uint) error {
|
||||||
|
return r.db.
|
||||||
|
Where("id = ?", id).
|
||||||
|
Delete(models.Alias{}).Error
|
||||||
|
}
|
||||||
|
|
||||||
|
func (r *AliasRepository) DeleteBatch(ids []uint) error {
|
||||||
|
return r.db.
|
||||||
|
Where("id IN ?", ids).
|
||||||
|
Delete(models.Alias{}).Error
|
||||||
|
}
|
||||||
|
@ -4,6 +4,8 @@ import (
|
|||||||
"errors"
|
"errors"
|
||||||
"github.com/muety/wakapi/models"
|
"github.com/muety/wakapi/models"
|
||||||
"gorm.io/gorm"
|
"gorm.io/gorm"
|
||||||
|
"gorm.io/gorm/clause"
|
||||||
|
"log"
|
||||||
)
|
)
|
||||||
|
|
||||||
type KeyValueRepository struct {
|
type KeyValueRepository struct {
|
||||||
@ -27,16 +29,19 @@ func (r *KeyValueRepository) GetString(key string) (*models.KeyStringValue, erro
|
|||||||
|
|
||||||
func (r *KeyValueRepository) PutString(kv *models.KeyStringValue) error {
|
func (r *KeyValueRepository) PutString(kv *models.KeyStringValue) error {
|
||||||
result := r.db.
|
result := r.db.
|
||||||
|
Clauses(clause.OnConflict{
|
||||||
|
UpdateAll: true,
|
||||||
|
}).
|
||||||
Where(&models.KeyStringValue{Key: kv.Key}).
|
Where(&models.KeyStringValue{Key: kv.Key}).
|
||||||
Assign(kv).
|
Assign(kv).
|
||||||
FirstOrCreate(kv)
|
Create(kv)
|
||||||
|
|
||||||
if err := result.Error; err != nil {
|
if err := result.Error; err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
if result.RowsAffected != 1 {
|
if result.RowsAffected != 1 {
|
||||||
return errors.New("nothing updated")
|
log.Printf("warning: did not insert key '%s', maybe just updated?\n", kv.Key)
|
||||||
}
|
}
|
||||||
|
|
||||||
return nil
|
return nil
|
||||||
|
@ -6,7 +6,13 @@ import (
|
|||||||
)
|
)
|
||||||
|
|
||||||
type IAliasRepository interface {
|
type IAliasRepository interface {
|
||||||
|
Insert(*models.Alias) (*models.Alias, error)
|
||||||
|
Delete(uint) error
|
||||||
|
DeleteBatch([]uint) error
|
||||||
GetByUser(string) ([]*models.Alias, error)
|
GetByUser(string) ([]*models.Alias, error)
|
||||||
|
GetByUserAndKey(string, string) ([]*models.Alias, error)
|
||||||
|
GetByUserAndKeyAndType(string, string, uint8) ([]*models.Alias, error)
|
||||||
|
GetByUserAndTypeAndValue(string, uint8, string) (*models.Alias, error)
|
||||||
}
|
}
|
||||||
|
|
||||||
type IHeartbeatRepository interface {
|
type IHeartbeatRepository interface {
|
||||||
|
@ -6,19 +6,24 @@ import (
|
|||||||
conf "github.com/muety/wakapi/config"
|
conf "github.com/muety/wakapi/config"
|
||||||
"github.com/muety/wakapi/models"
|
"github.com/muety/wakapi/models"
|
||||||
"github.com/muety/wakapi/models/view"
|
"github.com/muety/wakapi/models/view"
|
||||||
|
"github.com/muety/wakapi/services"
|
||||||
"net/http"
|
"net/http"
|
||||||
|
"strconv"
|
||||||
|
"time"
|
||||||
)
|
)
|
||||||
|
|
||||||
type HomeHandler struct {
|
type HomeHandler struct {
|
||||||
config *conf.Config
|
config *conf.Config
|
||||||
|
keyValueSrvc services.IKeyValueService
|
||||||
}
|
}
|
||||||
|
|
||||||
var loginDecoder = schema.NewDecoder()
|
var loginDecoder = schema.NewDecoder()
|
||||||
var signupDecoder = schema.NewDecoder()
|
var signupDecoder = schema.NewDecoder()
|
||||||
|
|
||||||
func NewHomeHandler() *HomeHandler {
|
func NewHomeHandler(keyValueService services.IKeyValueService) *HomeHandler {
|
||||||
return &HomeHandler{
|
return &HomeHandler{
|
||||||
config: conf.Get(),
|
config: conf.Get(),
|
||||||
|
keyValueSrvc: keyValueService,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -36,8 +41,25 @@ func (h *HomeHandler) GetIndex(w http.ResponseWriter, r *http.Request) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func (h *HomeHandler) buildViewModel(r *http.Request) *view.HomeViewModel {
|
func (h *HomeHandler) buildViewModel(r *http.Request) *view.HomeViewModel {
|
||||||
|
var totalHours int
|
||||||
|
var totalUsers int
|
||||||
|
|
||||||
|
if t, err := h.keyValueSrvc.GetString(conf.KeyLatestTotalTime); err == nil && t != nil && t.Value != "" {
|
||||||
|
if d, err := time.ParseDuration(t.Value); err == nil {
|
||||||
|
totalHours = int(d.Hours())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if t, err := h.keyValueSrvc.GetString(conf.KeyLatestTotalUsers); err == nil && t != nil && t.Value != "" {
|
||||||
|
if d, err := strconv.Atoi(t.Value); err == nil {
|
||||||
|
totalUsers = d
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
return &view.HomeViewModel{
|
return &view.HomeViewModel{
|
||||||
Success: r.URL.Query().Get("success"),
|
Success: r.URL.Query().Get("success"),
|
||||||
Error: r.URL.Query().Get("error"),
|
Error: r.URL.Query().Get("error"),
|
||||||
|
TotalHours: totalHours,
|
||||||
|
TotalUsers: totalUsers,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -2,7 +2,9 @@ package routes
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"fmt"
|
"fmt"
|
||||||
|
"github.com/markbates/pkger"
|
||||||
"github.com/muety/wakapi/config"
|
"github.com/muety/wakapi/config"
|
||||||
|
"github.com/muety/wakapi/models"
|
||||||
"github.com/muety/wakapi/utils"
|
"github.com/muety/wakapi/utils"
|
||||||
"html/template"
|
"html/template"
|
||||||
"io/ioutil"
|
"io/ioutil"
|
||||||
@ -17,12 +19,17 @@ func Init() {
|
|||||||
var templates map[string]*template.Template
|
var templates map[string]*template.Template
|
||||||
|
|
||||||
func loadTemplates() {
|
func loadTemplates() {
|
||||||
tplPath := "views"
|
const tplPath = "/views"
|
||||||
tpls := template.New("").Funcs(template.FuncMap{
|
tpls := template.New("").Funcs(template.FuncMap{
|
||||||
"json": utils.Json,
|
"json": utils.Json,
|
||||||
"date": utils.FormatDateHuman,
|
"date": utils.FormatDateHuman,
|
||||||
"title": strings.Title,
|
"title": strings.Title,
|
||||||
|
"join": strings.Join,
|
||||||
|
"add": utils.Add,
|
||||||
"capitalize": utils.Capitalize,
|
"capitalize": utils.Capitalize,
|
||||||
|
"toRunes": utils.ToRunes,
|
||||||
|
"entityTypes": models.SummaryTypes,
|
||||||
|
"typeName": typeName,
|
||||||
"getBasePath": func() string {
|
"getBasePath": func() string {
|
||||||
return config.Get().Server.BasePath
|
return config.Get().Server.BasePath
|
||||||
},
|
},
|
||||||
@ -30,7 +37,7 @@ func loadTemplates() {
|
|||||||
return config.Get().Version
|
return config.Get().Version
|
||||||
},
|
},
|
||||||
"getDbType": func() string {
|
"getDbType": func() string {
|
||||||
return strings.ToLower(config.Get().Db.Dialect)
|
return strings.ToLower(config.Get().Db.Type)
|
||||||
},
|
},
|
||||||
"htmlSafe": func(html string) template.HTML {
|
"htmlSafe": func(html string) template.HTML {
|
||||||
return template.HTML(html)
|
return template.HTML(html)
|
||||||
@ -38,7 +45,12 @@ func loadTemplates() {
|
|||||||
})
|
})
|
||||||
templates = make(map[string]*template.Template)
|
templates = make(map[string]*template.Template)
|
||||||
|
|
||||||
files, err := ioutil.ReadDir(tplPath)
|
dir, err := pkger.Open(tplPath)
|
||||||
|
if err != nil {
|
||||||
|
panic(err)
|
||||||
|
}
|
||||||
|
defer dir.Close()
|
||||||
|
files, err := dir.Readdir(0)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
panic(err)
|
panic(err)
|
||||||
}
|
}
|
||||||
@ -49,7 +61,17 @@ func loadTemplates() {
|
|||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
|
|
||||||
tpl, err := tpls.New(tplName).ParseFiles(fmt.Sprintf("%s/%s", tplPath, tplName))
|
templateFile, err := pkger.Open(fmt.Sprintf("%s/%s", tplPath, tplName))
|
||||||
|
if err != nil {
|
||||||
|
panic(err)
|
||||||
|
}
|
||||||
|
defer templateFile.Close()
|
||||||
|
template, err := ioutil.ReadAll(templateFile)
|
||||||
|
if err != nil {
|
||||||
|
panic(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
tpl, err := tpls.New(tplName).Parse(string(template))
|
||||||
if err != nil {
|
if err != nil {
|
||||||
panic(err)
|
panic(err)
|
||||||
}
|
}
|
||||||
@ -57,3 +79,22 @@ func loadTemplates() {
|
|||||||
templates[tplName] = tpl
|
templates[tplName] = tpl
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func typeName(t uint8) string {
|
||||||
|
if t == models.SummaryProject {
|
||||||
|
return "project"
|
||||||
|
}
|
||||||
|
if t == models.SummaryLanguage {
|
||||||
|
return "language"
|
||||||
|
}
|
||||||
|
if t == models.SummaryEditor {
|
||||||
|
return "editor"
|
||||||
|
}
|
||||||
|
if t == models.SummaryOS {
|
||||||
|
return "operating system"
|
||||||
|
}
|
||||||
|
if t == models.SummaryMachine {
|
||||||
|
return "machine"
|
||||||
|
}
|
||||||
|
return "unknown"
|
||||||
|
}
|
||||||
|
@ -17,16 +17,18 @@ type SettingsHandler struct {
|
|||||||
config *conf.Config
|
config *conf.Config
|
||||||
userSrvc services.IUserService
|
userSrvc services.IUserService
|
||||||
summarySrvc services.ISummaryService
|
summarySrvc services.ISummaryService
|
||||||
|
aliasSrvc services.IAliasService
|
||||||
aggregationSrvc services.IAggregationService
|
aggregationSrvc services.IAggregationService
|
||||||
languageMappingSrvc services.ILanguageMappingService
|
languageMappingSrvc services.ILanguageMappingService
|
||||||
}
|
}
|
||||||
|
|
||||||
var credentialsDecoder = schema.NewDecoder()
|
var credentialsDecoder = schema.NewDecoder()
|
||||||
|
|
||||||
func NewSettingsHandler(userService services.IUserService, summaryService services.ISummaryService, aggregationService services.IAggregationService, languageMappingService services.ILanguageMappingService) *SettingsHandler {
|
func NewSettingsHandler(userService services.IUserService, summaryService services.ISummaryService, aliasService services.IAliasService, aggregationService services.IAggregationService, languageMappingService services.ILanguageMappingService) *SettingsHandler {
|
||||||
return &SettingsHandler{
|
return &SettingsHandler{
|
||||||
config: conf.Get(),
|
config: conf.Get(),
|
||||||
summarySrvc: summaryService,
|
summarySrvc: summaryService,
|
||||||
|
aliasSrvc: aliasService,
|
||||||
aggregationSrvc: aggregationService,
|
aggregationSrvc: aggregationService,
|
||||||
languageMappingSrvc: languageMappingService,
|
languageMappingSrvc: languageMappingService,
|
||||||
userSrvc: userService,
|
userSrvc: userService,
|
||||||
@ -115,13 +117,17 @@ func (h *SettingsHandler) DeleteLanguageMapping(w http.ResponseWriter, r *http.R
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
mapping := &models.LanguageMapping{
|
if mapping, err := h.languageMappingSrvc.GetById(uint(id)); err != nil || mapping == nil {
|
||||||
ID: uint(id),
|
w.WriteHeader(http.StatusNotFound)
|
||||||
UserID: user.ID,
|
templates[conf.SettingsTemplate].Execute(w, h.buildViewModel(r).WithError("mapping not found"))
|
||||||
|
return
|
||||||
|
} else if mapping.UserID != user.ID {
|
||||||
|
w.WriteHeader(http.StatusForbidden)
|
||||||
|
templates[conf.SettingsTemplate].Execute(w, h.buildViewModel(r).WithError("not allowed to delete mapping"))
|
||||||
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
err = h.languageMappingSrvc.Delete(mapping)
|
if err := h.languageMappingSrvc.Delete(&models.LanguageMapping{ID: uint(id)}); err != nil {
|
||||||
if err != nil {
|
|
||||||
w.WriteHeader(http.StatusInternalServerError)
|
w.WriteHeader(http.StatusInternalServerError)
|
||||||
templates[conf.SettingsTemplate].Execute(w, h.buildViewModel(r).WithError("could not delete mapping"))
|
templates[conf.SettingsTemplate].Execute(w, h.buildViewModel(r).WithError("could not delete mapping"))
|
||||||
return
|
return
|
||||||
@ -157,6 +163,60 @@ func (h *SettingsHandler) PostLanguageMapping(w http.ResponseWriter, r *http.Req
|
|||||||
templates[conf.SettingsTemplate].Execute(w, h.buildViewModel(r).WithSuccess("mapping added successfully"))
|
templates[conf.SettingsTemplate].Execute(w, h.buildViewModel(r).WithSuccess("mapping added successfully"))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (h *SettingsHandler) DeleteAlias(w http.ResponseWriter, r *http.Request) {
|
||||||
|
if h.config.IsDev() {
|
||||||
|
loadTemplates()
|
||||||
|
}
|
||||||
|
|
||||||
|
user := r.Context().Value(models.UserKey).(*models.User)
|
||||||
|
aliasKey := r.PostFormValue("key")
|
||||||
|
aliasType, err := strconv.Atoi(r.PostFormValue("type"))
|
||||||
|
if err != nil {
|
||||||
|
aliasType = 99 // nothing will be found later on
|
||||||
|
}
|
||||||
|
|
||||||
|
if aliases, err := h.aliasSrvc.GetByUserAndKeyAndType(user.ID, aliasKey, uint8(aliasType)); err != nil {
|
||||||
|
w.WriteHeader(http.StatusNotFound)
|
||||||
|
templates[conf.SettingsTemplate].Execute(w, h.buildViewModel(r).WithError("aliases not found"))
|
||||||
|
return
|
||||||
|
} else if err := h.aliasSrvc.DeleteMulti(aliases); err != nil {
|
||||||
|
w.WriteHeader(http.StatusInternalServerError)
|
||||||
|
templates[conf.SettingsTemplate].Execute(w, h.buildViewModel(r).WithError("could not delete aliases"))
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
templates[conf.SettingsTemplate].Execute(w, h.buildViewModel(r).WithSuccess("aliases deleted successfully"))
|
||||||
|
}
|
||||||
|
|
||||||
|
func (h *SettingsHandler) PostAlias(w http.ResponseWriter, r *http.Request) {
|
||||||
|
if h.config.IsDev() {
|
||||||
|
loadTemplates()
|
||||||
|
}
|
||||||
|
user := r.Context().Value(models.UserKey).(*models.User)
|
||||||
|
aliasKey := r.PostFormValue("key")
|
||||||
|
aliasValue := r.PostFormValue("value")
|
||||||
|
aliasType, err := strconv.Atoi(r.PostFormValue("type"))
|
||||||
|
if err != nil {
|
||||||
|
aliasType = 99 // Alias.IsValid() will return false later on
|
||||||
|
}
|
||||||
|
|
||||||
|
alias := &models.Alias{
|
||||||
|
UserID: user.ID,
|
||||||
|
Key: aliasKey,
|
||||||
|
Value: aliasValue,
|
||||||
|
Type: uint8(aliasType),
|
||||||
|
}
|
||||||
|
|
||||||
|
if _, err := h.aliasSrvc.Create(alias); err != nil {
|
||||||
|
w.WriteHeader(http.StatusBadRequest)
|
||||||
|
// TODO: distinguish between bad request, conflict and server error
|
||||||
|
templates[conf.SettingsTemplate].Execute(w, h.buildViewModel(r).WithError("invalid input"))
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
templates[conf.SettingsTemplate].Execute(w, h.buildViewModel(r).WithSuccess("alias added successfully"))
|
||||||
|
}
|
||||||
|
|
||||||
func (h *SettingsHandler) PostResetApiKey(w http.ResponseWriter, r *http.Request) {
|
func (h *SettingsHandler) PostResetApiKey(w http.ResponseWriter, r *http.Request) {
|
||||||
if h.config.IsDev() {
|
if h.config.IsDev() {
|
||||||
loadTemplates()
|
loadTemplates()
|
||||||
@ -173,6 +233,21 @@ func (h *SettingsHandler) PostResetApiKey(w http.ResponseWriter, r *http.Request
|
|||||||
templates[conf.SettingsTemplate].Execute(w, h.buildViewModel(r).WithSuccess(msg))
|
templates[conf.SettingsTemplate].Execute(w, h.buildViewModel(r).WithSuccess(msg))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (h *SettingsHandler) PostSetWakatimeApiKey(w http.ResponseWriter, r *http.Request) {
|
||||||
|
if h.config.IsDev() {
|
||||||
|
loadTemplates()
|
||||||
|
}
|
||||||
|
|
||||||
|
user := r.Context().Value(models.UserKey).(*models.User)
|
||||||
|
if _, err := h.userSrvc.SetWakatimeApiKey(user, r.PostFormValue("api_key")); err != nil {
|
||||||
|
w.WriteHeader(http.StatusInternalServerError)
|
||||||
|
templates[conf.SettingsTemplate].Execute(w, h.buildViewModel(r).WithError("internal server error"))
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
templates[conf.SettingsTemplate].Execute(w, h.buildViewModel(r).WithSuccess("Wakatime API Key updated successfully"))
|
||||||
|
}
|
||||||
|
|
||||||
func (h *SettingsHandler) PostToggleBadges(w http.ResponseWriter, r *http.Request) {
|
func (h *SettingsHandler) PostToggleBadges(w http.ResponseWriter, r *http.Request) {
|
||||||
if h.config.IsDev() {
|
if h.config.IsDev() {
|
||||||
loadTemplates()
|
loadTemplates()
|
||||||
@ -216,9 +291,34 @@ func (h *SettingsHandler) PostRegenerateSummaries(w http.ResponseWriter, r *http
|
|||||||
func (h *SettingsHandler) buildViewModel(r *http.Request) *view.SettingsViewModel {
|
func (h *SettingsHandler) buildViewModel(r *http.Request) *view.SettingsViewModel {
|
||||||
user := r.Context().Value(models.UserKey).(*models.User)
|
user := r.Context().Value(models.UserKey).(*models.User)
|
||||||
mappings, _ := h.languageMappingSrvc.GetByUser(user.ID)
|
mappings, _ := h.languageMappingSrvc.GetByUser(user.ID)
|
||||||
|
aliases, _ := h.aliasSrvc.GetByUser(user.ID)
|
||||||
|
aliasMap := make(map[string][]*models.Alias)
|
||||||
|
for _, a := range aliases {
|
||||||
|
k := fmt.Sprintf("%s_%d", a.Key, a.Type)
|
||||||
|
if _, ok := aliasMap[k]; !ok {
|
||||||
|
aliasMap[k] = []*models.Alias{a}
|
||||||
|
} else {
|
||||||
|
aliasMap[k] = append(aliasMap[k], a)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
combinedAliases := make([]*view.SettingsVMCombinedAlias, 0)
|
||||||
|
for _, l := range aliasMap {
|
||||||
|
ca := &view.SettingsVMCombinedAlias{
|
||||||
|
Key: l[0].Key,
|
||||||
|
Type: l[0].Type,
|
||||||
|
Values: make([]string, len(l)),
|
||||||
|
}
|
||||||
|
for i, a := range l {
|
||||||
|
ca.Values[i] = a.Value
|
||||||
|
}
|
||||||
|
combinedAliases = append(combinedAliases, ca)
|
||||||
|
}
|
||||||
|
|
||||||
return &view.SettingsViewModel{
|
return &view.SettingsViewModel{
|
||||||
User: user,
|
User: user,
|
||||||
LanguageMappings: mappings,
|
LanguageMappings: mappings,
|
||||||
|
Aliases: combinedAliases,
|
||||||
Success: r.URL.Query().Get("success"),
|
Success: r.URL.Query().Get("success"),
|
||||||
Error: r.URL.Query().Get("error"),
|
Error: r.URL.Query().Get("error"),
|
||||||
}
|
}
|
||||||
|
@ -59,7 +59,7 @@ func (h *SummaryHandler) GetIndex(w http.ResponseWriter, r *http.Request) {
|
|||||||
|
|
||||||
vm := models.SummaryViewModel{
|
vm := models.SummaryViewModel{
|
||||||
Summary: summary,
|
Summary: summary,
|
||||||
LanguageColors: utils.FilterLanguageColors(h.config.App.LanguageColors, summary),
|
LanguageColors: utils.FilterLanguageColors(h.config.App.GetLanguageColors(), summary),
|
||||||
ApiKey: user.ApiKey,
|
ApiKey: user.ApiKey,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -11,6 +11,7 @@ from typing import List, Union
|
|||||||
import requests
|
import requests
|
||||||
from tqdm import tqdm
|
from tqdm import tqdm
|
||||||
|
|
||||||
|
MACHINE = "devmachine"
|
||||||
UA = 'wakatime/13.0.7 (Linux-4.15.0-91-generic-x86_64-with-glibc2.4) Python3.8.0.final.0 generator/1.42.1 generator-wakatime/4.0.0'
|
UA = 'wakatime/13.0.7 (Linux-4.15.0-91-generic-x86_64-with-glibc2.4) Python3.8.0.final.0 generator/1.42.1 generator-wakatime/4.0.0'
|
||||||
LANGUAGES = {
|
LANGUAGES = {
|
||||||
'Go': 'go',
|
'Go': 'go',
|
||||||
@ -75,7 +76,8 @@ def post_data_sync(data: List[Heartbeat], url: str, api_key: str):
|
|||||||
for h in tqdm(data):
|
for h in tqdm(data):
|
||||||
r = requests.post(url, json=[h.__dict__], headers={
|
r = requests.post(url, json=[h.__dict__], headers={
|
||||||
'User-Agent': UA,
|
'User-Agent': UA,
|
||||||
'Authorization': f'Basic {encoded_key}'
|
'Authorization': f'Basic {encoded_key}',
|
||||||
|
'X-Machine-Name': MACHINE,
|
||||||
})
|
})
|
||||||
if r.status_code != 201:
|
if r.status_code != 201:
|
||||||
print(r.text)
|
print(r.text)
|
||||||
|
@ -40,7 +40,7 @@ type AggregationJob struct {
|
|||||||
func (srv *AggregationService) Schedule() {
|
func (srv *AggregationService) Schedule() {
|
||||||
// Run once initially
|
// Run once initially
|
||||||
if err := srv.Run(nil); err != nil {
|
if err := srv.Run(nil); err != nil {
|
||||||
log.Fatalf("failed to run aggregation jobs: %v\n", err)
|
log.Fatalf("failed to run AggregationJob: %v\n", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
s := gocron.NewScheduler(time.Local)
|
s := gocron.NewScheduler(time.Local)
|
||||||
|
@ -1,11 +1,12 @@
|
|||||||
package services
|
package services
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"errors"
|
||||||
"github.com/muety/wakapi/config"
|
"github.com/muety/wakapi/config"
|
||||||
"github.com/muety/wakapi/repositories"
|
|
||||||
"sync"
|
|
||||||
|
|
||||||
"github.com/muety/wakapi/models"
|
"github.com/muety/wakapi/models"
|
||||||
|
"github.com/muety/wakapi/repositories"
|
||||||
|
"log"
|
||||||
|
"sync"
|
||||||
)
|
)
|
||||||
|
|
||||||
type AliasService struct {
|
type AliasService struct {
|
||||||
@ -22,7 +23,14 @@ func NewAliasService(aliasRepo repositories.IAliasRepository) *AliasService {
|
|||||||
|
|
||||||
var userAliases sync.Map
|
var userAliases sync.Map
|
||||||
|
|
||||||
func (srv *AliasService) LoadUserAliases(userId string) error {
|
func (srv *AliasService) IsInitialized(userId string) bool {
|
||||||
|
if _, ok := userAliases.Load(userId); ok {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
func (srv *AliasService) InitializeUser(userId string) error {
|
||||||
aliases, err := srv.repository.GetByUser(userId)
|
aliases, err := srv.repository.GetByUser(userId)
|
||||||
if err == nil {
|
if err == nil {
|
||||||
userAliases.Store(userId, aliases)
|
userAliases.Store(userId, aliases)
|
||||||
@ -30,9 +38,25 @@ func (srv *AliasService) LoadUserAliases(userId string) error {
|
|||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (srv *AliasService) GetByUser(userId string) ([]*models.Alias, error) {
|
||||||
|
aliases, err := srv.repository.GetByUser(userId)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
return aliases, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (srv *AliasService) GetByUserAndKeyAndType(userId, key string, summaryType uint8) ([]*models.Alias, error) {
|
||||||
|
aliases, err := srv.repository.GetByUserAndKeyAndType(userId, key, summaryType)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
return aliases, nil
|
||||||
|
}
|
||||||
|
|
||||||
func (srv *AliasService) GetAliasOrDefault(userId string, summaryType uint8, value string) (string, error) {
|
func (srv *AliasService) GetAliasOrDefault(userId string, summaryType uint8, value string) (string, error) {
|
||||||
if !srv.IsInitialized(userId) {
|
if !srv.IsInitialized(userId) {
|
||||||
if err := srv.LoadUserAliases(userId); err != nil {
|
if err := srv.InitializeUser(userId); err != nil {
|
||||||
return "", err
|
return "", err
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -46,9 +70,46 @@ func (srv *AliasService) GetAliasOrDefault(userId string, summaryType uint8, val
|
|||||||
return value, nil
|
return value, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (srv *AliasService) IsInitialized(userId string) bool {
|
func (srv *AliasService) Create(alias *models.Alias) (*models.Alias, error) {
|
||||||
if _, ok := userAliases.Load(userId); ok {
|
result, err := srv.repository.Insert(alias)
|
||||||
return true
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
go srv.reinitUser(alias.UserID)
|
||||||
|
return result, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (srv *AliasService) Delete(alias *models.Alias) error {
|
||||||
|
if alias.UserID == "" {
|
||||||
|
return errors.New("no user id specified")
|
||||||
|
}
|
||||||
|
err := srv.repository.Delete(alias.ID)
|
||||||
|
go srv.reinitUser(alias.UserID)
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
func (srv *AliasService) DeleteMulti(aliases []*models.Alias) error {
|
||||||
|
ids := make([]uint, len(aliases))
|
||||||
|
affectedUsers := make(map[string]bool)
|
||||||
|
for i, a := range aliases {
|
||||||
|
if a.UserID == "" {
|
||||||
|
return errors.New("no user id specified")
|
||||||
|
}
|
||||||
|
affectedUsers[a.UserID] = true
|
||||||
|
ids[i] = a.ID
|
||||||
|
}
|
||||||
|
|
||||||
|
err := srv.repository.DeleteBatch(ids)
|
||||||
|
|
||||||
|
for k := range affectedUsers {
|
||||||
|
go srv.reinitUser(k)
|
||||||
|
}
|
||||||
|
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
func (srv *AliasService) reinitUser(userId string) {
|
||||||
|
if err := srv.InitializeUser(userId); err != nil {
|
||||||
|
log.Printf("error initializing user aliases – %v\n", err)
|
||||||
}
|
}
|
||||||
return false
|
|
||||||
}
|
}
|
||||||
|
@ -1,6 +1,7 @@
|
|||||||
package services
|
package services
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"errors"
|
||||||
"github.com/muety/wakapi/config"
|
"github.com/muety/wakapi/config"
|
||||||
"github.com/muety/wakapi/models"
|
"github.com/muety/wakapi/models"
|
||||||
"github.com/muety/wakapi/repositories"
|
"github.com/muety/wakapi/repositories"
|
||||||
@ -18,7 +19,7 @@ func NewLanguageMappingService(languageMappingsRepo repositories.ILanguageMappin
|
|||||||
return &LanguageMappingService{
|
return &LanguageMappingService{
|
||||||
config: config.Get(),
|
config: config.Get(),
|
||||||
repository: languageMappingsRepo,
|
repository: languageMappingsRepo,
|
||||||
cache: cache.New(1*time.Hour, 2*time.Hour),
|
cache: cache.New(24*time.Hour, 24*time.Hour),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -63,11 +64,15 @@ func (srv *LanguageMappingService) Create(mapping *models.LanguageMapping) (*mod
|
|||||||
}
|
}
|
||||||
|
|
||||||
func (srv *LanguageMappingService) Delete(mapping *models.LanguageMapping) error {
|
func (srv *LanguageMappingService) Delete(mapping *models.LanguageMapping) error {
|
||||||
|
if mapping.UserID == "" {
|
||||||
|
return errors.New("no user id specified")
|
||||||
|
}
|
||||||
err := srv.repository.Delete(mapping.ID)
|
err := srv.repository.Delete(mapping.ID)
|
||||||
srv.cache.Delete(mapping.UserID)
|
srv.cache.Delete(mapping.UserID)
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
func (srv LanguageMappingService) getServerMappings() map[string]string {
|
func (srv *LanguageMappingService) getServerMappings() map[string]string {
|
||||||
return srv.config.App.CustomLanguages
|
// https://dave.cheney.net/2017/04/30/if-a-map-isnt-a-reference-variable-what-is-it
|
||||||
|
return srv.config.App.GetCustomLanguages()
|
||||||
}
|
}
|
||||||
|
119
services/misc.go
Normal file
119
services/misc.go
Normal file
@ -0,0 +1,119 @@
|
|||||||
|
package services
|
||||||
|
|
||||||
|
import (
|
||||||
|
"github.com/muety/wakapi/config"
|
||||||
|
"go.uber.org/atomic"
|
||||||
|
"log"
|
||||||
|
"runtime"
|
||||||
|
"strconv"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"github.com/go-co-op/gocron"
|
||||||
|
"github.com/muety/wakapi/models"
|
||||||
|
)
|
||||||
|
|
||||||
|
type MiscService struct {
|
||||||
|
config *config.Config
|
||||||
|
userService IUserService
|
||||||
|
summaryService ISummaryService
|
||||||
|
keyValueService IKeyValueService
|
||||||
|
jobCount atomic.Uint32
|
||||||
|
}
|
||||||
|
|
||||||
|
func NewMiscService(userService IUserService, summaryService ISummaryService, keyValueService IKeyValueService) *MiscService {
|
||||||
|
return &MiscService{
|
||||||
|
config: config.Get(),
|
||||||
|
userService: userService,
|
||||||
|
summaryService: summaryService,
|
||||||
|
keyValueService: keyValueService,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
type CountTotalTimeJob struct {
|
||||||
|
UserID string
|
||||||
|
NumJobs int
|
||||||
|
}
|
||||||
|
|
||||||
|
type CountTotalTimeResult struct {
|
||||||
|
UserId string
|
||||||
|
Total time.Duration
|
||||||
|
}
|
||||||
|
|
||||||
|
func (srv *MiscService) ScheduleCountTotalTime() {
|
||||||
|
// Run once initially
|
||||||
|
if err := srv.runCountTotalTime(); err != nil {
|
||||||
|
log.Fatalf("failed to run CountTotalTimeJob: %v\n", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
s := gocron.NewScheduler(time.Local)
|
||||||
|
s.Every(1).Day().At(srv.config.App.CountingTime).Do(srv.runCountTotalTime)
|
||||||
|
s.StartBlocking()
|
||||||
|
}
|
||||||
|
|
||||||
|
func (srv *MiscService) runCountTotalTime() error {
|
||||||
|
jobs := make(chan *CountTotalTimeJob)
|
||||||
|
results := make(chan *CountTotalTimeResult)
|
||||||
|
|
||||||
|
defer close(jobs)
|
||||||
|
|
||||||
|
for i := 0; i < runtime.NumCPU(); i++ {
|
||||||
|
go srv.countTotalTimeWorker(jobs, results)
|
||||||
|
}
|
||||||
|
|
||||||
|
go srv.persistTotalTimeWorker(results)
|
||||||
|
|
||||||
|
// generate the jobs
|
||||||
|
if users, err := srv.userService.GetAll(); err == nil {
|
||||||
|
for _, u := range users {
|
||||||
|
jobs <- &CountTotalTimeJob{
|
||||||
|
UserID: u.ID,
|
||||||
|
NumJobs: len(users),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (srv *MiscService) countTotalTimeWorker(jobs <-chan *CountTotalTimeJob, results chan<- *CountTotalTimeResult) {
|
||||||
|
for job := range jobs {
|
||||||
|
if result, err := srv.summaryService.Aliased(time.Time{}, time.Now(), &models.User{ID: job.UserID}, srv.summaryService.Retrieve); err != nil {
|
||||||
|
log.Printf("Failed to count total for user %s: %v.\n", job.UserID, err)
|
||||||
|
} else {
|
||||||
|
log.Printf("Successfully counted total for user %s.\n", job.UserID)
|
||||||
|
results <- &CountTotalTimeResult{
|
||||||
|
UserId: job.UserID,
|
||||||
|
Total: result.TotalTime(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if srv.jobCount.Inc() == uint32(job.NumJobs) {
|
||||||
|
srv.jobCount.Store(0)
|
||||||
|
close(results)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (srv *MiscService) persistTotalTimeWorker(results <-chan *CountTotalTimeResult) {
|
||||||
|
var c int
|
||||||
|
var total time.Duration
|
||||||
|
for result := range results {
|
||||||
|
total += result.Total
|
||||||
|
c++
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := srv.keyValueService.PutString(&models.KeyStringValue{
|
||||||
|
Key: config.KeyLatestTotalTime,
|
||||||
|
Value: total.String(),
|
||||||
|
}); err != nil {
|
||||||
|
log.Printf("Failed to save total time count: %v\n", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := srv.keyValueService.PutString(&models.KeyStringValue{
|
||||||
|
Key: config.KeyLatestTotalUsers,
|
||||||
|
Value: strconv.Itoa(c),
|
||||||
|
}); err != nil {
|
||||||
|
log.Printf("Failed to save total users count: %v\n", err)
|
||||||
|
}
|
||||||
|
}
|
@ -10,10 +10,19 @@ type IAggregationService interface {
|
|||||||
Run(map[string]bool) error
|
Run(map[string]bool) error
|
||||||
}
|
}
|
||||||
|
|
||||||
|
type IMiscService interface {
|
||||||
|
ScheduleCountTotalTime()
|
||||||
|
}
|
||||||
|
|
||||||
type IAliasService interface {
|
type IAliasService interface {
|
||||||
LoadUserAliases(string) error
|
Create(*models.Alias) (*models.Alias, error)
|
||||||
GetAliasOrDefault(string, uint8, string) (string, error)
|
Delete(*models.Alias) error
|
||||||
|
DeleteMulti([]*models.Alias) error
|
||||||
IsInitialized(string) bool
|
IsInitialized(string) bool
|
||||||
|
InitializeUser(string) error
|
||||||
|
GetByUser(string) ([]*models.Alias, error)
|
||||||
|
GetByUserAndKeyAndType(string, string, uint8) ([]*models.Alias, error)
|
||||||
|
GetAliasOrDefault(string, uint8, string) (string, error)
|
||||||
}
|
}
|
||||||
|
|
||||||
type IHeartbeatService interface {
|
type IHeartbeatService interface {
|
||||||
@ -54,5 +63,6 @@ type IUserService interface {
|
|||||||
Update(*models.User) (*models.User, error)
|
Update(*models.User) (*models.User, error)
|
||||||
ResetApiKey(*models.User) (*models.User, error)
|
ResetApiKey(*models.User) (*models.User, error)
|
||||||
ToggleBadges(*models.User) (*models.User, error)
|
ToggleBadges(*models.User) (*models.User, error)
|
||||||
|
SetWakatimeApiKey(*models.User, string) (*models.User, error)
|
||||||
MigrateMd5Password(*models.User, *models.Login) (*models.User, error)
|
MigrateMd5Password(*models.User, *models.Login) (*models.User, error)
|
||||||
}
|
}
|
||||||
|
@ -50,7 +50,7 @@ func (srv *SummaryService) Aliased(from, to time.Time, user *models.User, f Summ
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Initialize alias resolver service
|
// Initialize alias resolver service
|
||||||
if err := srv.aliasService.LoadUserAliases(user.ID); err != nil {
|
if err := srv.aliasService.InitializeUser(user.ID); err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -68,7 +68,7 @@ func (srv *SummaryService) Aliased(from, to time.Time, user *models.User, f Summ
|
|||||||
|
|
||||||
func (srv *SummaryService) Retrieve(from, to time.Time, user *models.User) (*models.Summary, error) {
|
func (srv *SummaryService) Retrieve(from, to time.Time, user *models.User) (*models.Summary, error) {
|
||||||
// Check cache
|
// Check cache
|
||||||
cacheKey := srv.getHash(from.String(), to.String(), user.ID, "--aliased")
|
cacheKey := srv.getHash(from.String(), to.String(), user.ID)
|
||||||
if cacheResult, ok := srv.cache.Get(cacheKey); ok {
|
if cacheResult, ok := srv.cache.Get(cacheKey); ok {
|
||||||
return cacheResult.(*models.Summary), nil
|
return cacheResult.(*models.Summary), nil
|
||||||
}
|
}
|
||||||
|
@ -249,7 +249,7 @@ func (suite *SummaryServiceTestSuite) TestSummaryService_Aliased() {
|
|||||||
|
|
||||||
from, to = suite.TestStartTime, suite.TestStartTime.Add(1*time.Hour)
|
from, to = suite.TestStartTime, suite.TestStartTime.Add(1*time.Hour)
|
||||||
suite.HeartbeatService.On("GetAllWithin", from, to, suite.TestUser).Return(filter(from, to, suite.TestHeartbeats), nil)
|
suite.HeartbeatService.On("GetAllWithin", from, to, suite.TestUser).Return(filter(from, to, suite.TestHeartbeats), nil)
|
||||||
suite.AliasService.On("LoadUserAliases", TestUserId).Return(nil)
|
suite.AliasService.On("InitializeUser", TestUserId).Return(nil)
|
||||||
suite.AliasService.On("GetAliasOrDefault", TestUserId, models.SummaryProject, TestProject1).Return(TestProject2, nil)
|
suite.AliasService.On("GetAliasOrDefault", TestUserId, models.SummaryProject, TestProject1).Return(TestProject2, nil)
|
||||||
suite.AliasService.On("GetAliasOrDefault", TestUserId, mock.Anything, mock.Anything).Return("", nil)
|
suite.AliasService.On("GetAliasOrDefault", TestUserId, mock.Anything, mock.Anything).Return("", nil)
|
||||||
|
|
||||||
|
@ -5,11 +5,14 @@ import (
|
|||||||
"github.com/muety/wakapi/models"
|
"github.com/muety/wakapi/models"
|
||||||
"github.com/muety/wakapi/repositories"
|
"github.com/muety/wakapi/repositories"
|
||||||
"github.com/muety/wakapi/utils"
|
"github.com/muety/wakapi/utils"
|
||||||
|
"github.com/patrickmn/go-cache"
|
||||||
uuid "github.com/satori/go.uuid"
|
uuid "github.com/satori/go.uuid"
|
||||||
|
"time"
|
||||||
)
|
)
|
||||||
|
|
||||||
type UserService struct {
|
type UserService struct {
|
||||||
Config *config.Config
|
Config *config.Config
|
||||||
|
cache *cache.Cache
|
||||||
repository repositories.IUserRepository
|
repository repositories.IUserRepository
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -17,15 +20,36 @@ func NewUserService(userRepo repositories.IUserRepository) *UserService {
|
|||||||
return &UserService{
|
return &UserService{
|
||||||
Config: config.Get(),
|
Config: config.Get(),
|
||||||
repository: userRepo,
|
repository: userRepo,
|
||||||
|
cache: cache.New(1*time.Hour, 2*time.Hour),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func (srv *UserService) GetUserById(userId string) (*models.User, error) {
|
func (srv *UserService) GetUserById(userId string) (*models.User, error) {
|
||||||
return srv.repository.GetById(userId)
|
if u, ok := srv.cache.Get(userId); ok {
|
||||||
|
return u.(*models.User), nil
|
||||||
|
}
|
||||||
|
|
||||||
|
u, err := srv.repository.GetById(userId)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
srv.cache.Set(u.ID, u, cache.DefaultExpiration)
|
||||||
|
return u, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (srv *UserService) GetUserByKey(key string) (*models.User, error) {
|
func (srv *UserService) GetUserByKey(key string) (*models.User, error) {
|
||||||
return srv.repository.GetByApiKey(key)
|
if u, ok := srv.cache.Get(key); ok {
|
||||||
|
return u.(*models.User), nil
|
||||||
|
}
|
||||||
|
|
||||||
|
u, err := srv.repository.GetByApiKey(key)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
srv.cache.Set(u.ID, u, cache.DefaultExpiration)
|
||||||
|
return u, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (srv *UserService) GetAll() ([]*models.User, error) {
|
func (srv *UserService) GetAll() ([]*models.User, error) {
|
||||||
@ -49,19 +73,28 @@ func (srv *UserService) CreateOrGet(signup *models.Signup) (*models.User, bool,
|
|||||||
}
|
}
|
||||||
|
|
||||||
func (srv *UserService) Update(user *models.User) (*models.User, error) {
|
func (srv *UserService) Update(user *models.User) (*models.User, error) {
|
||||||
|
srv.cache.Flush()
|
||||||
return srv.repository.Update(user)
|
return srv.repository.Update(user)
|
||||||
}
|
}
|
||||||
|
|
||||||
func (srv *UserService) ResetApiKey(user *models.User) (*models.User, error) {
|
func (srv *UserService) ResetApiKey(user *models.User) (*models.User, error) {
|
||||||
|
srv.cache.Flush()
|
||||||
user.ApiKey = uuid.NewV4().String()
|
user.ApiKey = uuid.NewV4().String()
|
||||||
return srv.Update(user)
|
return srv.Update(user)
|
||||||
}
|
}
|
||||||
|
|
||||||
func (srv *UserService) ToggleBadges(user *models.User) (*models.User, error) {
|
func (srv *UserService) ToggleBadges(user *models.User) (*models.User, error) {
|
||||||
|
srv.cache.Flush()
|
||||||
return srv.repository.UpdateField(user, "badges_enabled", !user.BadgesEnabled)
|
return srv.repository.UpdateField(user, "badges_enabled", !user.BadgesEnabled)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (srv *UserService) SetWakatimeApiKey(user *models.User, apiKey string) (*models.User, error) {
|
||||||
|
srv.cache.Flush()
|
||||||
|
return srv.repository.UpdateField(user, "wakatime_api_key", apiKey)
|
||||||
|
}
|
||||||
|
|
||||||
func (srv *UserService) MigrateMd5Password(user *models.User, login *models.Login) (*models.User, error) {
|
func (srv *UserService) MigrateMd5Password(user *models.User, login *models.Login) (*models.User, error) {
|
||||||
|
srv.cache.Flush()
|
||||||
user.Password = login.Password
|
user.Password = login.Password
|
||||||
if hash, err := utils.HashBcrypt(user.Password, srv.Config.Security.PasswordSalt); err != nil {
|
if hash, err := utils.HashBcrypt(user.Password, srv.Config.Security.PasswordSalt); err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
|
@ -18,6 +18,10 @@ func FormatDateHuman(date time.Time) string {
|
|||||||
return date.Format("Mon, 02 Jan 2006 15:04")
|
return date.Format("Mon, 02 Jan 2006 15:04")
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func Add(i, j int) int {
|
||||||
|
return i + j
|
||||||
|
}
|
||||||
|
|
||||||
func ParseUserAgent(ua string) (string, string, error) {
|
func ParseUserAgent(ua string) (string, string, error) {
|
||||||
re := regexp.MustCompile(`(?iU)^wakatime\/[\d+.]+\s\((\w+)-.*\)\s.+\s([^\/\s]+)-wakatime\/.+$`)
|
re := regexp.MustCompile(`(?iU)^wakatime\/[\d+.]+\s\((\w+)-.*\)\s.+\s([^\/\s]+)-wakatime\/.+$`)
|
||||||
groups := re.FindAllStringSubmatch(ua, -1)
|
groups := re.FindAllStringSubmatch(ua, -1)
|
||||||
|
@ -12,3 +12,10 @@ func Json(data interface{}) template.JS {
|
|||||||
}
|
}
|
||||||
return template.JS(d)
|
return template.JS(d)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func ToRunes(s string) (r []string) {
|
||||||
|
for _, c := range []rune(s) {
|
||||||
|
r = append(r, string(c))
|
||||||
|
}
|
||||||
|
return r
|
||||||
|
}
|
||||||
|
@ -1 +1 @@
|
|||||||
1.18.0
|
1.20.1
|
@ -1,2 +1,2 @@
|
|||||||
<script src="https://cdnjs.cloudflare.com/ajax/libs/seedrandom/3.0.5/seedrandom.min.js"></script>
|
<script src="https://cdnjs.cloudflare.com/ajax/libs/seedrandom/3.0.5/seedrandom.min.js" integrity="sha384-bFS5CG904xYIgxBcrDF4KFNXuM7KeSGsSvS/QTaDqMTEdbaaxjg2Y2TSU3Ygs7wG" crossorigin="anonymous"></script>
|
||||||
<script src="https://cdnjs.cloudflare.com/ajax/libs/Chart.js/2.9.4/Chart.bundle.min.js"></script>
|
<script src="https://cdnjs.cloudflare.com/ajax/libs/Chart.js/2.9.4/Chart.bundle.min.js" integrity="sha384-mZ3q69BYmd4GxHp59G3RrsaFdWDxVSoqd7oVYuWRm2qiXrduT63lQtlhdD9lKbm3" crossorigin="anonymous"></script>
|
@ -7,6 +7,6 @@
|
|||||||
<link rel="icon" type="image/png" sizes="16x16" href="assets/images/favicon-16x16.png">
|
<link rel="icon" type="image/png" sizes="16x16" href="assets/images/favicon-16x16.png">
|
||||||
<link rel="manifest" href="assets/site.webmanifest">
|
<link rel="manifest" href="assets/site.webmanifest">
|
||||||
<link href="https://fonts.googleapis.com/css?family=Roboto&display=swap" rel="stylesheet">
|
<link href="https://fonts.googleapis.com/css?family=Roboto&display=swap" rel="stylesheet">
|
||||||
<link href="https://unpkg.com/tailwindcss@^1.4.6/dist/tailwind.min.css" rel="stylesheet">
|
<link href="https://unpkg.com/tailwindcss@^1.4.6/dist/tailwind.min.css" rel="stylesheet" integrity="sha384-g+D4mP3VJaTdOzGhEGvSbCkMsmTWyibnPpolg6g7Xnw9Z5y6PbFl7W3CW6iA8Ybm" crossorigin="anonymous">
|
||||||
<link href="assets/app.css" rel="stylesheet">
|
<link href="assets/app.css" rel="stylesheet">
|
||||||
</head>
|
</head>
|
@ -11,21 +11,40 @@
|
|||||||
|
|
||||||
<div class="absolute flex top-0 right-0 mr-8 mt-10 py-2">
|
<div class="absolute flex top-0 right-0 mr-8 mt-10 py-2">
|
||||||
<div class="mx-1">
|
<div class="mx-1">
|
||||||
<a href="login" class="py-1 px-3 h-8 block rounded border border-green-700 text-white text-sm">🔑 Login️</a>
|
<a href="login" class="py-1 px-3 h-8 block rounded border border-green-700 text-white text-sm">🔑
|
||||||
|
Login️</a>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<main class="mt-10 flex-grow flex justify-center w-full">
|
<main class="mt-10 flex-grow flex justify-center w-full">
|
||||||
<div class="flex flex-col text-white">
|
<div class="flex flex-col text-white">
|
||||||
<h1 class="text-4xl font-semibold antialiased text-center mb-2">Keep Track of <span class="text-green-700">Your</span> Coding Time 🕓</h1>
|
<h1 class="text-4xl font-semibold antialiased text-center mb-2">Keep Track of <span
|
||||||
<p class="text-center text-gray-500 text-xl my-2">Wakapi is an open-source tool that helps you keep track of the time you have spent coding on different projects in different programming languages and more. Ideal for statistics freaks any anyone else.</p>
|
class="text-green-700">Your</span> Coding Time 🕓</h1>
|
||||||
|
<p class="text-center text-gray-500 text-xl my-2">Wakapi is an open-source tool that helps you keep track of the
|
||||||
|
time you have spent coding on different projects in different programming languages and more. Ideal for
|
||||||
|
statistics freaks any anyone else.</p>
|
||||||
|
|
||||||
|
<p class="text-center text-gray-500 text-xl my-4">
|
||||||
|
<span class="mr-1">💡 The system has tracked a total of </span>
|
||||||
|
{{ range $d := .TotalHours | printf "%d" | toRunes }}
|
||||||
|
<span class="bg-gray-900 rounded-sm p-1 border border-gray-700 font-mono" style="margin: auto -2px;" title="{{ $.TotalHours }} hours (updated once a day)">{{ $d }}</span>
|
||||||
|
{{ end }}
|
||||||
|
<span class="mx-1">hours of coding from</span>
|
||||||
|
{{ range $d := .TotalUsers | printf "%d" | toRunes }}
|
||||||
|
<span class="bg-gray-900 rounded-sm p-1 border border-gray-700 font-mono" style="margin: auto -2px;" title="{{ $.TotalUsers }} users (updated once a day)">{{ $d }}</span>
|
||||||
|
{{ end }}
|
||||||
|
<span class="ml-1">users.</span>
|
||||||
|
</p>
|
||||||
|
|
||||||
<div class="flex justify-center mt-4 mb-8 space-x-2">
|
<div class="flex justify-center mt-4 mb-8 space-x-2">
|
||||||
<a href="login">
|
<a href="login">
|
||||||
<button type="button" class="py-1 px-3 rounded bg-green-700 hover:bg-green-800 text-white font-semibold">🚀 Try it!</button>
|
<button type="button"
|
||||||
|
class="py-1 px-3 rounded bg-green-700 hover:bg-green-800 text-white font-semibold">🚀 Try it!
|
||||||
|
</button>
|
||||||
</a>
|
</a>
|
||||||
<a href="https://github.com/muety/wakapi#%EF%B8%8F-server-setup" target="_blank" rel="noopener noreferrer">
|
<a href="https://github.com/muety/wakapi#%EF%B8%8F-server-setup" target="_blank" rel="noopener noreferrer">
|
||||||
<button type="button" class="py-1 px-3 h-8 rounded border border-green-700 text-white">📡 Host it</button>
|
<button type="button" class="py-1 px-3 h-8 rounded border border-green-700 text-white">📡 Host it
|
||||||
|
</button>
|
||||||
</a>
|
</a>
|
||||||
<a href="https://github.com/muety/wakapi" target="_blank" rel="noopener noreferrer">
|
<a href="https://github.com/muety/wakapi" target="_blank" rel="noopener noreferrer">
|
||||||
<button type="button" class="py-1 px-3 h-8 rounded border border-green-700 text-white">
|
<button type="button" class="py-1 px-3 h-8 rounded border border-green-700 text-white">
|
||||||
@ -47,17 +66,24 @@
|
|||||||
<li>✅ Fancy statistics and plots</li>
|
<li>✅ Fancy statistics and plots</li>
|
||||||
<li>✅ Cool badges for readmes</li>
|
<li>✅ Cool badges for readmes</li>
|
||||||
<li>✅ Intuitive REST API</li>
|
<li>✅ Intuitive REST API</li>
|
||||||
<li>✅ Compatible with <a href="https://wakatime.com" target="_blank" rel="noopener noreferrer" class="underline">Wakatime</a></li>
|
<li>✅ Compatible with <a href="https://wakatime.com" target="_blank"
|
||||||
<li>✅ <a href="https://prometheus.io" target="_blank" rel="noopener noreferrer" class="underline">Prometheus</a> metrics via <a href="https://github.com/MacroPower/wakatime_exporter" target="_blank" rel="noopener noreferrer" class="underline">exporter</a></li>
|
rel="noopener noreferrer" class="underline">Wakatime</a></li>
|
||||||
|
<li>✅ <a href="https://prometheus.io" target="_blank" rel="noopener noreferrer"
|
||||||
|
class="underline">Prometheus</a> metrics via <a
|
||||||
|
href="https://github.com/MacroPower/wakatime_exporter" target="_blank"
|
||||||
|
rel="noopener noreferrer" class="underline">exporter</a></li>
|
||||||
<li>✅ Self-hosted</li>
|
<li>✅ Self-hosted</li>
|
||||||
</ul>
|
</ul>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="flex justify-center space-x-2 mt-12">
|
<div class="flex justify-center space-x-2 mt-12">
|
||||||
<img alt="License badge" src="https://badges.fw-web.space/github/license/muety/wakapi?color=%232F855A&style=flat-square">
|
<img alt="License badge"
|
||||||
<img alt="Go version badge" src="https://badges.fw-web.space/github/go-mod/go-version/muety/wakapi?color=%232F855A&style=flat-square">
|
src="https://badges.fw-web.space/github/license/muety/wakapi?color=%232F855A&style=flat-square">
|
||||||
<img alt="Wakapi coding time badge" src="https://badges.fw-web.space/endpoint?color=%232F855A&style=flat-square&label=wakapi&url=https://wakapi.dev/api/compat/shields/v1/n1try/interval:any/project:wakapi">
|
<img alt="Go version badge"
|
||||||
|
src="https://badges.fw-web.space/github/go-mod/go-version/muety/wakapi?color=%232F855A&style=flat-square">
|
||||||
|
<img alt="Wakapi coding time badge"
|
||||||
|
src="https://badges.fw-web.space/endpoint?color=%232F855A&style=flat-square&label=wakapi&url=https://wakapi.dev/api/compat/shields/v1/n1try/interval:any/project:wakapi">
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</main>
|
</main>
|
||||||
|
@ -20,7 +20,7 @@
|
|||||||
<label class="inline-block text-sm mb-1 text-gray-500" for="username">Username</label>
|
<label class="inline-block text-sm mb-1 text-gray-500" for="username">Username</label>
|
||||||
<input class="shadow appearance-none bg-gray-800 focus:bg-gray-700 text-gray-300 border-green-700 focus:border-gray-500 border rounded w-full py-1 px-3"
|
<input class="shadow appearance-none bg-gray-800 focus:bg-gray-700 text-gray-300 border-green-700 focus:border-gray-500 border rounded w-full py-1 px-3"
|
||||||
type="text" id="username"
|
type="text" id="username"
|
||||||
name="username" placeholder="Enter your username" minlength="3" required autofocus>
|
name="username" placeholder="Enter your username" minlength="1" required autofocus>
|
||||||
</div>
|
</div>
|
||||||
<div class="mb-8">
|
<div class="mb-8">
|
||||||
<label class="inline-block text-sm mb-1 text-gray-500" for="password">Password</label>
|
<label class="inline-block text-sm mb-1 text-gray-500" for="password">Password</label>
|
||||||
|
@ -5,6 +5,20 @@
|
|||||||
|
|
||||||
<body class="bg-gray-800 text-gray-700 p-4 pt-10 flex flex-col min-h-screen max-w-screen-xl mx-auto justify-center">
|
<body class="bg-gray-800 text-gray-700 p-4 pt-10 flex flex-col min-h-screen max-w-screen-xl mx-auto justify-center">
|
||||||
|
|
||||||
|
<style>
|
||||||
|
.inline-bullet-list li a {
|
||||||
|
text-decoration: underline;
|
||||||
|
}
|
||||||
|
|
||||||
|
.inline-bullet-list li:after {
|
||||||
|
content: "•";
|
||||||
|
}
|
||||||
|
|
||||||
|
.inline-bullet-list li:last-child:after {
|
||||||
|
content: "";
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
|
||||||
{{ template "header.tpl.html" . }}
|
{{ template "header.tpl.html" . }}
|
||||||
|
|
||||||
<div class="w-full flex justify-center">
|
<div class="w-full flex justify-center">
|
||||||
@ -19,11 +33,37 @@
|
|||||||
|
|
||||||
<main class="mt-4 flex-grow flex justify-center w-full">
|
<main class="mt-4 flex-grow flex justify-center w-full">
|
||||||
<div class="flex flex-col flex-grow max-w-xl mt-8">
|
<div class="flex flex-col flex-grow max-w-xl mt-8">
|
||||||
<div class="w-full my-8 pb-8 border-b border-gray-700">
|
<div class="text-gray-500 text-xs mb-8">
|
||||||
<div class="font-semibold text-lg text-white m-0 border-b-2 border-green-700 inline-block">
|
<ul class="flex justify-center flex-wrap space-x-1 inline-bullet-list px-12">
|
||||||
Change Password
|
<li class="hover:text-gray-400 mb-1">
|
||||||
|
<a href="settings#password">Change Password</a>
|
||||||
|
</li>
|
||||||
|
<li class="hover:text-gray-400 mb-1">
|
||||||
|
<a href="settings#apikey">Reset API Key</a>
|
||||||
|
</li>
|
||||||
|
<li class="hover:text-gray-400 mb-1">
|
||||||
|
<a href="settings#aliases">Aliases</a>
|
||||||
|
</li>
|
||||||
|
<li class="hover:text-gray-400 mb-1">
|
||||||
|
<a href="settings#languages">Languages & File Extensions</a>
|
||||||
|
</li>
|
||||||
|
<li class="hover:text-gray-400 mb-1">
|
||||||
|
<a href="settings#badges">Badges</a>
|
||||||
|
</li>
|
||||||
|
<li class="hover:text-gray-400 mb-1">
|
||||||
|
<a href="settings#integrations">Integrations</a>
|
||||||
|
</li>
|
||||||
|
<li class="hover:text-gray-400 mb-1">
|
||||||
|
<a href="settings#danger">Danger Zone</a>
|
||||||
|
</li>
|
||||||
|
</ul>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<div class="w-full my-8 pb-8 border-b border-gray-700">
|
||||||
|
<h2 class="font-semibold text-lg text-white m-0 border-b-2 border-green-700 inline-block" id="password">
|
||||||
|
Change Password
|
||||||
|
</h2>
|
||||||
|
|
||||||
<form class="mt-10" action="settings/credentials" method="post">
|
<form class="mt-10" action="settings/credentials" method="post">
|
||||||
<div class="mb-8">
|
<div class="mb-8">
|
||||||
<label class="inline-block text-sm mb-1 text-gray-500" for="password_old">Current Password</label>
|
<label class="inline-block text-sm mb-1 text-gray-500" for="password_old">Current Password</label>
|
||||||
@ -52,13 +92,15 @@
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="w-full mt-4 mb-8 pb-8 border-b border-gray-700">
|
<div class="w-full mt-4 mb-8 pb-8 border-b border-gray-700">
|
||||||
<div class="font-semibold text-lg text-white m-0 border-b-2 border-green-700 inline-block">
|
<h2 class="font-semibold text-lg text-white m-0 border-b-2 border-green-700 inline-block" id="apikey">
|
||||||
Reset API Key
|
Reset API Key
|
||||||
</div>
|
</h2>
|
||||||
|
|
||||||
<form class="mt-6" action="settings/reset" method="post">
|
<form class="mt-6" action="settings/reset" method="post">
|
||||||
<div class="text-gray-300 text-sm mb-4">
|
<div class="text-gray-300 text-sm mb-4">
|
||||||
<strong>⚠️ Caution:</strong> Resetting your API key requires you to update your <span class="font-mono">.wakatime.cfg</span> files on all of your computers to make the WakaTime client send heartbeats again.
|
<strong>⚠️ Caution:</strong> Resetting your API key requires you to update your <span
|
||||||
|
class="font-mono">.wakatime.cfg</span> files on all of your computers to make the WakaTime
|
||||||
|
client send heartbeats again.
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="flex justify-between float-right">
|
<div class="flex justify-between float-right">
|
||||||
@ -69,72 +111,147 @@
|
|||||||
</form>
|
</form>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="w-full mt-4 mb-8 pb-8 border-b border-gray-700">
|
<div class="w-full mt-4 mb-8 pb-8 border-b border-gray-700" id="aliases">
|
||||||
<div class="font-semibold text-lg text-white m-0 border-b-2 border-green-700 inline-block">
|
<h2 class="font-semibold text-lg text-white m-0 border-b-2 border-green-700 inline-block">
|
||||||
Language Mappings
|
Aliases
|
||||||
</div>
|
</h2>
|
||||||
|
|
||||||
<div class="text-gray-300 text-sm mb-4 mt-6">
|
<div class="text-gray-300 text-sm mb-4 mt-6">
|
||||||
You can specify custom mapping from file extensions to programming languages (e.g. a <span class="text-xs bg-gray-900 rounded py-1 px-2 font-mono">.jsx</span> file could be mapped to <span class="text-xs bg-gray-900 rounded py-1 px-2 font-mono">React</span>.)
|
You can specify aliases for any type of entity. For instance, you can define a rule, that both <span
|
||||||
|
class="inline-block mb-1 text-gray-500 italic">myapp-frontend</span> and <span
|
||||||
|
class="inline-block mb-1 text-gray-500 italic">myapp-backend</span> are combined under a
|
||||||
|
project called <span class="inline-block mb-1 text-gray-500 italic">myapp</span>.
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{{ if .LanguageMappings }}
|
{{ if .Aliases }}
|
||||||
{{ range $i, $mapping := .LanguageMappings }}
|
<h3 class="inline-block font-semibold text-md border-b border-green-700 text-white">Rules</h3>
|
||||||
<div class="text-white border-1 w-full border-green-700 inline-block my-1 py-1 text-align">
|
{{ range $i, $alias := .Aliases }}
|
||||||
<label class="inline-block text-sm mb-1 text-gray-500" >When filename ends in:</label>
|
<div class="flex items-center">
|
||||||
{{ $mapping.Extension }}
|
<div class="text-gray-500 border-1 w-full border-green-700 inline-block my-1 py-1 text-align text-sm"
|
||||||
<label class="inline-block text-sm mb-1 text-gray-500" >Change the language to:</label>
|
style="line-height: 1.8">
|
||||||
{{ $mapping.Language }}
|
▸ All <span class="underline">{{ $alias.Type | typeName }}s</span> named
|
||||||
|
{{ range $j, $value := $alias.Values }}
|
||||||
<form class="float-right" action="settings/language_mappings/delete" method="post">
|
<span class="text-white text-xs bg-gray-900 rounded py-1 px-2 font-mono">{{- $value -}}</span>
|
||||||
<input type="hidden" id="mapping_id" name="mapping_id" required value="{{ $mapping.ID }}">
|
{{ if lt $j (add (len $alias.Values) -2) }}
|
||||||
<button type="submit" class="py-1 px-3 rounded bg-red-500 hover:bg-red-600 text-white text-sm">
|
<span class="-ml-1">{{- ", " | capitalize -}}</span>
|
||||||
Remove
|
{{ else if lt $j (add (len $alias.Values) -1) }}
|
||||||
|
<span>{{- "or" -}}</span>
|
||||||
|
{{ end }}
|
||||||
|
{{ end }}
|
||||||
|
are mapped to <span class="underline">{{ $alias.Type | typeName }}</span> <span
|
||||||
|
class="text-white text-xs bg-gray-900 rounded py-1 px-2 font-mono">{{ $alias.Key }}</span>.
|
||||||
|
</div>
|
||||||
|
<form class="float-right" action="settings/aliases/delete" method="post">
|
||||||
|
<input type="hidden" id="delete_alias_key" name="key" required value="{{ $alias.Key }}">
|
||||||
|
<input type="hidden" id="delete_alias_type" name="type" required value="{{ $alias.Type }}">
|
||||||
|
<button type="submit"
|
||||||
|
class="py-1 px-3 rounded border border-red-500 hover:border-red-600 text-gray-400 text-sm">
|
||||||
|
✕
|
||||||
</button>
|
</button>
|
||||||
</form>
|
</form>
|
||||||
</div>
|
</div>
|
||||||
{{end}}
|
{{end}}
|
||||||
{{else}}
|
<div class="mb-8"></div>
|
||||||
<div class="text-white border-1 w-full border-green-700 inline-block my-1 py-1">
|
|
||||||
No rules.
|
|
||||||
</div>
|
|
||||||
{{end}}
|
{{end}}
|
||||||
|
|
||||||
<form action="settings/language_mappings" method="post">
|
<h3 class="inline-block font-semibold text-md border-b border-green-700 text-white mb-2">Add Rule</h3>
|
||||||
<div class="inline-block justify-around mt-4 w-full">
|
<form action="settings/aliases" method="post">
|
||||||
<label class="inline-block text-sm mb-1 text-gray-500" for="extension">When filename ends in:</label>
|
<div class="flex items-center mt-2 w-full text-gray-500 text-sm">
|
||||||
<input class="shadow appearance-nonshadow appearance-none bg-gray-800 focus:bg-gray-700 text-gray-300 border-green-700 focus:border-gray-500 border rounded w-full py-1 px-3"
|
<span class="mr-2">Map</span>
|
||||||
type="text" id="extension"
|
<select name="type" id="select-type"
|
||||||
name="extension" placeholder=".py" minlength="1" required>
|
class="shadow appearance-nonshadow appearance-none bg-gray-800 focus:bg-gray-700 text-gray-300 border-green-700 focus:border-gray-500 border rounded py-1 px-3 cursor-pointer">
|
||||||
</div>
|
{{ range $i, $t := entityTypes }}
|
||||||
<div class="inline-block justify-around mt-4 w-full">
|
<option value="{{ $t }}">{{ $t | typeName | capitalize }}</option>
|
||||||
<label class="inline-block text-sm mb-1 text-gray-500" for="language">Change the language to:</label>
|
{{ end }}
|
||||||
<input class="shadow appearance-nonshadow appearance-none bg-gray-800 focus:bg-gray-700 text-gray-300 border-green-700 focus:border-gray-500 border rounded w-full py-1 px-3"
|
</select>
|
||||||
type="text" id="language"
|
<span class="mx-2">named</span>
|
||||||
name="language" placeholder="Python" minlength="1" required>
|
<input class="shadow appearance-nonshadow appearance-none bg-gray-800 focus:bg-gray-700 text-gray-300 border-green-700 focus:border-gray-500 border rounded py-1 px-3"
|
||||||
</div>
|
type="text" id="alias-value" style="width: 130px;"
|
||||||
<div class="flex justify-between float-right">
|
name="value" placeholder="myapp-frontend" minlength="1" required>
|
||||||
<button type="submit" class="py-1 px-3 my-3 rounded bg-green-700 hover:bg-green-800 text-white text-sm">
|
<span class="mx-2">to</span>
|
||||||
|
<input class="shadow appearance-nonshadow appearance-none bg-gray-800 focus:bg-gray-700 text-gray-300 border-green-700 focus:border-gray-500 border rounded py-1 px-3"
|
||||||
|
type="text" id="alias-key" style="width: 100px"
|
||||||
|
name="key" placeholder="myapp" minlength="1" required>
|
||||||
|
<div class="flex-grow flex justify-end">
|
||||||
|
<button type="submit"
|
||||||
|
class="py-1 px-3 rounded bg-green-700 hover:bg-green-800 text-white text-sm">
|
||||||
Add
|
Add
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
|
</div>
|
||||||
</form>
|
</form>
|
||||||
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="w-full mt-4 mb-8 pb-8 border-b border-gray-700">
|
<div class="w-full mt-4 mb-8 pb-8 border-b border-gray-700">
|
||||||
<div class="font-semibold text-lg text-white m-0 border-b-2 border-green-700 inline-block">
|
<div class="font-semibold text-lg text-white m-0 border-b-2 border-green-700 inline-block" id="languages">
|
||||||
|
Languages & File Extensions
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="text-gray-300 text-sm mb-4 mt-6">
|
||||||
|
You can specify custom mapping from file extensions to programming languages, for instance a <span
|
||||||
|
class="inline-block mb-1 text-gray-500 italic">.jsx</span> file could be mapped to the <span
|
||||||
|
class="inline-block mb-1 text-gray-500 italic">React</span> language.
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{{ if .LanguageMappings }}
|
||||||
|
<h3 class="inline-block font-semibold text-md border-b border-green-700 text-white">Rules</h3>
|
||||||
|
{{ range $i, $mapping := .LanguageMappings }}
|
||||||
|
<div class="flex items-center">
|
||||||
|
<div class="text-gray-500 border-1 w-full border-green-700 inline-block my-1 py-1 text-align text-sm">
|
||||||
|
▸ When filename ends in <span
|
||||||
|
class="text-white text-xs bg-gray-900 rounded py-1 px-2 font-mono mr-1">{{ $mapping.Extension }}</span>
|
||||||
|
then change the <span class="underline">language</span> to <span
|
||||||
|
class="text-white text-xs bg-gray-900 rounded py-1 px-2 font-mono mr-1">{{ $mapping.Language }}</span>
|
||||||
|
</div>
|
||||||
|
<form class="float-right" action="settings/language_mappings/delete" method="post">
|
||||||
|
<input type="hidden" id="mapping_id" name="mapping_id" required value="{{ $mapping.ID }}">
|
||||||
|
<button type="submit"
|
||||||
|
class="py-1 px-3 rounded border border-red-500 hover:border-red-600 text-gray-400 text-sm">
|
||||||
|
✕
|
||||||
|
</button>
|
||||||
|
</form>
|
||||||
|
</div>
|
||||||
|
{{end}}
|
||||||
|
<div class="mb-8"></div>
|
||||||
|
{{end}}
|
||||||
|
|
||||||
|
<h3 class="inline-block font-semibold text-md border-b border-green-700 text-white">Add Rule</h3>
|
||||||
|
<form action="settings/language_mappings" method="post">
|
||||||
|
<div class="flex items-center w-full text-gray-500 text-sm">
|
||||||
|
<span class="mr-2">When filename ends in</span>
|
||||||
|
<input class="shadow appearance-nonshadow appearance-none bg-gray-800 focus:bg-gray-700 text-gray-300 border-green-700 focus:border-gray-500 border rounded py-1 px-3"
|
||||||
|
type="text" id="extension" style="width: 70px"
|
||||||
|
name="extension" placeholder=".py" minlength="1" required>
|
||||||
|
<span class="mx-2">change language to</span>
|
||||||
|
<input class="shadow appearance-nonshadow appearance-none bg-gray-800 focus:bg-gray-700 text-gray-300 border-green-700 focus:border-gray-500 border rounded py-1 px-3"
|
||||||
|
type="text" id="language" style="width: 100px"
|
||||||
|
name="language" placeholder="Python" minlength="1" required>
|
||||||
|
<div class="flex-grow flex justify-end">
|
||||||
|
<button type="submit"
|
||||||
|
class="py-1 px-3 my-3 rounded bg-green-700 hover:bg-green-800 text-white text-sm">
|
||||||
|
Add
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</form>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="w-full mt-4 mb-8 pb-8 border-b border-gray-700">
|
||||||
|
<div class="font-semibold text-lg text-white m-0 border-b-2 border-green-700 inline-block" id="badges">
|
||||||
Badges
|
Badges
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<form class="mt-6" action="settings/badges" method="post">
|
<form class="mt-6" action="settings/badges" method="post">
|
||||||
<div class="text-gray-300 text-sm mb-4">
|
<div class="text-gray-300 text-sm mb-4">
|
||||||
{{ if .User.BadgesEnabled }}
|
{{ if .User.BadgesEnabled }}
|
||||||
<p>Badges are currently enabled. You can disable the feature by deactivating the respective API endpoint.</p>
|
<p>Badges are currently enabled. You can disable the feature by deactivating the respective API
|
||||||
|
endpoint.</p>
|
||||||
|
|
||||||
<div class="flex justify-around mt-4">
|
<div class="flex justify-around mt-4">
|
||||||
<span class="font-mono font-normal bg-gray-900 p-1 rounded whitespace-no-wrap">GET /api/compat/shields/v1</span>
|
<span class="font-mono font-normal bg-gray-900 p-1 rounded whitespace-no-wrap">GET /api/compat/shields/v1</span>
|
||||||
<button type="submit" class="py-1 px-2 rounded bg-orange-700 hover:bg-orange-800 text-white text-xs" title="Disable support for badges to secure endpoint">
|
<button type="submit"
|
||||||
|
class="py-1 px-2 rounded bg-orange-700 hover:bg-orange-800 text-white text-xs"
|
||||||
|
title="Disable support for badges to secure endpoint">
|
||||||
Status: public
|
Status: public
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
@ -143,28 +260,40 @@
|
|||||||
<div class="flex flex-col mb-4">
|
<div class="flex flex-col mb-4">
|
||||||
<div class="flex justify-between my-2">
|
<div class="flex justify-between my-2">
|
||||||
<div>
|
<div>
|
||||||
<img class="with-url-src" src="https://img.shields.io/endpoint?url=%s/api/compat/shields/v1/{{ .User.ID }}/interval:today&style=flat-square&color=blue&label=today" alt="Shields.io badge"/>
|
<img class="with-url-src"
|
||||||
|
src="https://img.shields.io/endpoint?url=%s/api/compat/shields/v1/{{ .User.ID }}/interval:today&style=flat-square&color=blue&label=today"
|
||||||
|
alt="Shields.io badge"/>
|
||||||
</div>
|
</div>
|
||||||
<span class="with-url-inner text-xs bg-gray-900 rounded py-1 px-2 font-mono whitespace-no-wrap overflow-auto" style="max-width: 300px;">
|
<span class="with-url-inner text-xs bg-gray-900 rounded py-1 px-2 font-mono whitespace-no-wrap overflow-auto"
|
||||||
|
style="max-width: 300px;">
|
||||||
https://img.shields.io/endpoint?url=%s/api/compat/shields/v1/{{ .User.ID }}/interval:today&style=flat-square&color=blue&label=today
|
https://img.shields.io/endpoint?url=%s/api/compat/shields/v1/{{ .User.ID }}/interval:today&style=flat-square&color=blue&label=today
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
<div class="flex justify-between my-2">
|
<div class="flex justify-between my-2">
|
||||||
<div>
|
<div>
|
||||||
<img class="with-url-src" src="https://img.shields.io/endpoint?url=%s/api/compat/shields/v1/{{ .User.ID }}/interval:30_days&style=flat-square&color=blue&label=last 30d" alt="Shields.io badge"/>
|
<img class="with-url-src"
|
||||||
|
src="https://img.shields.io/endpoint?url=%s/api/compat/shields/v1/{{ .User.ID }}/interval:30_days&style=flat-square&color=blue&label=last 30d"
|
||||||
|
alt="Shields.io badge"/>
|
||||||
</div>
|
</div>
|
||||||
<span class="with-url-inner text-xs bg-gray-900 rounded py-1 px-2 font-mono whitespace-no-wrap overflow-auto" style="max-width: 300px;">
|
<span class="with-url-inner text-xs bg-gray-900 rounded py-1 px-2 font-mono whitespace-no-wrap overflow-auto"
|
||||||
|
style="max-width: 300px;">
|
||||||
https://img.shields.io/endpoint?url=%s/api/compat/shields/v1/{{ .User.ID }}/interval:30_days&style=flat-square&color=blue&label=last 30d
|
https://img.shields.io/endpoint?url=%s/api/compat/shields/v1/{{ .User.ID }}/interval:30_days&style=flat-square&color=blue&label=last 30d
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<p>You can also add <span class="text-xs bg-gray-900 rounded py-1 px-2 font-mono">/project:your-cool-project</span> to the URL to filter by project.</p>
|
<p>You can also add <span class="text-xs bg-gray-900 rounded py-1 px-2 font-mono">/project:your-cool-project</span>
|
||||||
|
to the URL to filter by project.</p>
|
||||||
{{ else }}
|
{{ else }}
|
||||||
<p>You have the ability to create badges from your coding statistics using <a href="https://shields.io" target="_blank" class="border-b border-green-800" rel="noopener noreferrer">Shields.io</a>. To do so, you need to grant public, unauthorized access to the respective endpoint.</p>
|
<p>You have the ability to create badges from your coding statistics using <a
|
||||||
|
href="https://shields.io" target="_blank" class="border-b border-green-800"
|
||||||
|
rel="noopener noreferrer">Shields.io</a>. To do so, you need to grant public, unauthorized
|
||||||
|
access to the respective endpoint.</p>
|
||||||
<div class="flex justify-around mt-4">
|
<div class="flex justify-around mt-4">
|
||||||
<span class="font-mono font-normal bg-gray-900 p-1 rounded whitespace-no-wrap">GET /api/compat/shields/v1</span>
|
<span class="font-mono font-normal bg-gray-900 p-1 rounded whitespace-no-wrap">GET /api/compat/shields/v1</span>
|
||||||
<button type="submit" class="py-1 px-2 rounded bg-green-700 hover:bg-green-800 text-white text-xs" title="Make endpoint public to enable badges">
|
<button type="submit"
|
||||||
|
class="py-1 px-2 rounded bg-green-700 hover:bg-green-800 text-white text-xs"
|
||||||
|
title="Make endpoint public to enable badges">
|
||||||
Status: protected
|
Status: protected
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
@ -173,8 +302,70 @@
|
|||||||
</form>
|
</form>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<div class="w-full mt-4 mb-8 pb-8 border-b border-gray-700">
|
||||||
|
<h2 class="font-semibold text-lg text-white m-0 border-b-2 border-green-700 inline-block" id="integrations">
|
||||||
|
Integrations
|
||||||
|
</h2>
|
||||||
|
|
||||||
|
<div class="mt-10 text-gray-300 text-sm">
|
||||||
|
<h3 class="inline-block font-semibold text-md mb-4 border-b border-green-700">
|
||||||
|
WakaTime
|
||||||
|
</h3>
|
||||||
|
|
||||||
|
<div class="flex space-x-4">
|
||||||
|
<img alt="WakaTime Logo"
|
||||||
|
width="55px"
|
||||||
|
src="data:image/svg+xml;base64,PHN2ZyB3aWR0aD0iMzQwIiBoZWlnaHQ9IjM0MCIgdmlld0JveD0iMCAwIDM0MCAzNDAiIGZpbGw9Im5vbmUiIHhtbG5zPSJodHRwOi8vd3d3LnczLm9yZy8yMDAwL3N2ZyI+CjxwYXRoIGZpbGwtcnVsZT0iZXZlbm9kZCIgY2xpcC1ydWxlPSJldmVub2RkIiBkPSJNMTcwIDIwQzg3LjE1NiAyMCAyMCA4Ny4xNTYgMjAgMTcwQzIwIDI1Mi44NDQgODcuMTU2IDMyMCAxNzAgMzIwQzI1Mi44NDQgMzIwIDMyMCAyNTIuODQ0IDMyMCAxNzBDMzIwIDg3LjE1NiAyNTIuODQ0IDIwIDE3MCAyMFYyMFYyMFoiIHN0cm9rZT0id2hpdGUiIHN0cm9rZS13aWR0aD0iNDAiLz4KPHBhdGggZD0iTTE5MC4xODMgMjEzLjU0MUMxODguNzQgMjE1LjQ0MyAxODYuNTc2IDIxNi42NjcgMTg0LjE1MSAyMTYuNjY3QzE4My45MTMgMjE2LjY2NyAxODMuNjc3IDIxNi42NTEgMTgzLjQ0MyAyMTYuNjI3QzE4My4wNDIgMjE2LjU3OSAxODIuODIzIDIxNi41NDUgMTgyLjYwNiAyMTYuNDk3QzE4Mi4zMzcgMjE2LjQzNCAxODIuMTM3IDIxNi4zNzUgMTgxLjk0IDIxNi4zMDhDMTgxLjU2MSAyMTYuMTc2IDE4MS4zOTIgMjE2LjEwOSAxODEuMjI4IDIxNi4wMzVDMTgwLjg0MyAyMTUuODQ5IDE4MC43MDcgMjE1Ljc3OCAxODAuNTcyIDIxNS43MDFDMTgwLjIwNSAyMTUuNDc4IDE4MC4xMDkgMjE1LjQxMiAxODAuMDE0IDIxNS4zNDVDMTc5Ljg1NiAyMTUuMjMzIDE3OS42OTggMjE1LjExNyAxNzkuNTQ3IDIxNC45OTJDMTc5LjI1MSAyMTQuNzQ2IDE3OS4xNDcgMjE0LjY1IDE3OS4wNDQgMjE0LjU1MkMxNzguNzMxIDIxNC4yNDEgMTc4LjUzMSAyMTQuMDE4IDE3OC4zNDEgMjEzLjc4NUMxNzcuOTgyIDIxMy4zMzEgMTc3LjY5IDIxMi44ODggMTc3LjQzOCAyMTIuNDE1TDE2OC42IDE5OC4yMTRMMTU5Ljc2NiAyMTIuNDE1QzE1OC4zOCAyMTQuOTM5IDE1NS44NzQgMjE2LjY2NyAxNTIuOTk1IDIxNi42NjdDMTUwLjEwNiAyMTYuNjY3IDE0Ny41ODggMjE0LjkyNiAxNDYuMjQzIDIxMi4zNDZMMTA3LjYwNyAxNTYuMDYxQzEwNi4zMzcgMTU0LjUyOSAxMDUuNTU2IDE1Mi40OTkgMTA1LjU1NiAxNTAuMjU4QzEwNS41NTYgMTQ1LjUxNCAxMDkuMDQzIDE0MS42NjUgMTEzLjM0NCAxNDEuNjY1QzExNi4xMjcgMTQxLjY2NSAxMTguNTY0IDE0My4yODIgMTE5Ljk0MiAxNDUuNzA4TDE1Mi41NTUgMTkzLjlMMTYxLjczNSAxNzguOTUyQzE2My4wNTggMTc2LjI4OCAxNjUuNjI2IDE3NC40NzggMTY4LjU3NSAxNzQuNDc4QzE3MS4yNzMgMTc0LjQ3OCAxNzMuNjUyIDE3NS45OTYgMTc1LjA0OSAxNzguMjk4TDE4NC41MTcgMTkzLjgzOUwyMzUuNjg0IDEyMC41ODNDMjM3LjA3NSAxMTguMjI2IDIzOS40NzUgMTE2LjY2NyAyNDIuMjEzIDExNi42NjdDMjQ2LjUxNCAxMTYuNjY3IDI1MCAxMjAuNTE0IDI1MCAxMjUuMjU4QzI1MCAxMjcuMzMyIDI0OS4zMzcgMTI5LjIzMiAyNDguMjMgMTMwLjcxNUwxOTAuMTgzIDIxMy41NDFWMjEzLjU0MVoiIGZpbGw9IndoaXRlIiBzdHJva2U9IndoaXRlIiBzdHJva2Utd2lkdGg9IjEwIi8+Cjwvc3ZnPgo=">
|
||||||
|
<p class="text-sm">You can connect Wakapi with the official <a class="underline"
|
||||||
|
href="https://wakatime.com"
|
||||||
|
rel="noopener noreferrer"
|
||||||
|
target="_blank">WakaTime</a> in a way
|
||||||
|
that all heartbeats sent to Wakapi are relayed. This way, you can use both services
|
||||||
|
at
|
||||||
|
the same time. To get started, <a class="underline"
|
||||||
|
href="https://wakatime.com/developers#authentication"
|
||||||
|
rel="noopener noreferrer"
|
||||||
|
target="_blank">get your API key</a> and paste it here.</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<form action="settings/wakatime_integration" method="post">
|
||||||
|
|
||||||
|
{{ $placeholderText := "Paste your WakaTime API key here ..." }}
|
||||||
|
{{ if .User.WakatimeApiKey }}
|
||||||
|
{{ $placeholderText = "********" }}
|
||||||
|
{{ end }}
|
||||||
|
|
||||||
|
<div class="flex items-center mt-8 space-x-2">
|
||||||
|
<label class="text-gray-500 font-semibold">API Key:</label>
|
||||||
|
<input type="password" name="api_key" id="wakatime_api_key"
|
||||||
|
class="flex-grow shadow appearance-nonshadow appearance-none bg-gray-800 text-gray-300 border-green-700 border rounded py-1 px-3 {{ if not .User.WakatimeApiKey }}focus:bg-gray-700 focus:border-gray-500{{ end }}"
|
||||||
|
placeholder="{{ $placeholderText }}" {{ if .User.WakatimeApiKey }}readonly{{ end }}>
|
||||||
|
<div class="flex-grow flex justify-end">
|
||||||
|
{{ if not .User.WakatimeApiKey }}
|
||||||
|
<button type="submit"
|
||||||
|
class="py-1 px-3 my-3 rounded bg-green-700 hover:bg-green-800 text-white text-sm">
|
||||||
|
Connect
|
||||||
|
</button>
|
||||||
|
{{ else }}
|
||||||
|
<button type="submit"
|
||||||
|
class="py-1 px-3 rounded bg-red-500 hover:bg-red-600 text-white text-sm">Disconnect
|
||||||
|
</button>
|
||||||
|
{{ end }}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</form>
|
||||||
|
|
||||||
|
<p class="mt-6">
|
||||||
|
<span class="font-semibold">👉 Please note:</span>
|
||||||
|
<span>When enabling this feature, the operators of this server will, in theory (!), have unlimited access to your data stored in WakaTime. If you are concerned about your privacy, please do not enable this integration or wait for OAuth 2 authentication (<a
|
||||||
|
class="underline" target="_blank" href="https://github.com/muety/wakapi/issues/94" rel="noopener noreferrer">#94</a>) to be implemented.</span>
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
|
||||||
<div class="w-full mt-4 mb-8 pb-8">
|
<div class="w-full mt-4 mb-8 pb-8">
|
||||||
<div class="font-semibold text-lg text-white m-0 border-b-2 border-green-700 inline-block">
|
<div class="font-semibold text-lg text-white m-0 border-b-2 border-green-700 inline-block" id="danger">
|
||||||
⚠️ Danger Zone
|
⚠️ Danger Zone
|
||||||
</div>
|
</div>
|
||||||
<div class="mt-10 text-gray-300 text-sm">
|
<div class="mt-10 text-gray-300 text-sm">
|
||||||
@ -182,19 +373,26 @@
|
|||||||
Regenerate summaries
|
Regenerate summaries
|
||||||
</h3>
|
</h3>
|
||||||
<p>
|
<p>
|
||||||
Wakapi improves its efficiency and speed by automatically aggregating individual heartbeats to summaries on a per-day basis.
|
Wakapi improves its efficiency and speed by automatically aggregating individual heartbeats to
|
||||||
That is, historic summaries, i.e. such from past days, are generated once and only fetched from the database in a static fashion afterwards, unless you pass <span class="font-mono font-normal bg-gray-900 p-1 rounded whitespace-no-wrap">&recompute=true</span> with your request.
|
summaries on a per-day basis.
|
||||||
|
That is, historic summaries, i.e. such from past days, are generated once and only fetched from the
|
||||||
|
database in a static fashion afterwards, unless you pass <span
|
||||||
|
class="font-mono font-normal bg-gray-900 p-1 rounded whitespace-no-wrap">&recompute=true</span>
|
||||||
|
with your request.
|
||||||
</p>
|
</p>
|
||||||
<p class="mt-2">
|
<p class="mt-2">
|
||||||
If, for some reason, these aggregated summaries are faulty or preconditions have change (e.g. you modified language mappings retrospectively), you may want to re-generate them from raw heartbeats.
|
If, for some reason, these aggregated summaries are faulty or preconditions have change (e.g. you
|
||||||
|
modified language mappings retrospectively), you may want to re-generate them from raw heartbeats.
|
||||||
</p>
|
</p>
|
||||||
<p class="mt-2">
|
<p class="mt-2">
|
||||||
<strong>Note:</strong> Only run this action if you know what you are doing. Data might be lost is case heartbeats were deleted after the respective summaries had been generated.
|
<strong>Note:</strong> Only run this action if you know what you are doing. Data might be lost is
|
||||||
|
case heartbeats were deleted after the respective summaries had been generated.
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
<div class="mt-10 flex justify-center">
|
<div class="mt-10 flex justify-center">
|
||||||
<form action="settings/regenerate" method="post" id="form-regenerate-summaries">
|
<form action="settings/regenerate" method="post" id="form-regenerate-summaries">
|
||||||
<button type="button" class="py-1 px-3 rounded bg-red-500 hover:bg-red-600 text-white text-sm" id="btn-regenerate-summaries">
|
<button type="button" class="py-1 px-3 rounded bg-red-500 hover:bg-red-600 text-white text-sm"
|
||||||
|
id="btn-regenerate-summaries">
|
||||||
Clear & Regenerate
|
Clear & Regenerate
|
||||||
</button>
|
</button>
|
||||||
</form>
|
</form>
|
||||||
@ -214,7 +412,7 @@
|
|||||||
e.classList.remove('hidden')
|
e.classList.remove('hidden')
|
||||||
})
|
})
|
||||||
|
|
||||||
const btnRegenerate = document.querySelector("#btn-regenerate-summaries")
|
const btnRegenerate = document.querySelector('#btn-regenerate-summaries')
|
||||||
const formRegenerate = document.querySelector('#form-regenerate-summaries')
|
const formRegenerate = document.querySelector('#form-regenerate-summaries')
|
||||||
btnRegenerate.addEventListener('click', () => {
|
btnRegenerate.addEventListener('click', () => {
|
||||||
if (confirm('Are you sure?')) {
|
if (confirm('Are you sure?')) {
|
||||||
|
@ -23,7 +23,7 @@
|
|||||||
rel="noopener noreferrer"
|
rel="noopener noreferrer"
|
||||||
class="border-b border-green-700">WakaTime</a>
|
class="border-b border-green-700">WakaTime</a>
|
||||||
client tools.
|
client tools.
|
||||||
Please refer to <a href="https://github.com/muety/wakapi#client-setup" target="_blank"
|
Please refer to <a href="https://github.com/muety/wakapi#-client-setup" target="_blank"
|
||||||
rel="noopener noreferrer"
|
rel="noopener noreferrer"
|
||||||
class="border-b border-green-700">this readme section</a> for instructions.
|
class="border-b border-green-700">this readme section</a> for instructions.
|
||||||
You will be able to view you <strong>API Key</strong> once you log in.
|
You will be able to view you <strong>API Key</strong> once you log in.
|
||||||
@ -35,7 +35,7 @@
|
|||||||
<label class="inline-block text-sm mb-1 text-gray-500" for="username">Username</label>
|
<label class="inline-block text-sm mb-1 text-gray-500" for="username">Username</label>
|
||||||
<input class="shadow appearance-none bg-gray-800 focus:bg-gray-700 text-gray-300 border-green-700 focus:border-gray-500 border rounded w-full py-1 px-3"
|
<input class="shadow appearance-none bg-gray-800 focus:bg-gray-700 text-gray-300 border-green-700 focus:border-gray-500 border rounded w-full py-1 px-3"
|
||||||
type="text" id="username"
|
type="text" id="username"
|
||||||
name="username" placeholder="Choose a username" minlength="3" required autofocus>
|
name="username" placeholder="Choose a username" minlength="1" required autofocus>
|
||||||
</div>
|
</div>
|
||||||
<div class="mb-8">
|
<div class="mb-8">
|
||||||
<label class="inline-block text-sm mb-1 text-gray-500" for="password">Password</label>
|
<label class="inline-block text-sm mb-1 text-gray-500" for="password">Password</label>
|
||||||
|
Reference in New Issue
Block a user