diff --git a/backend/Makefile b/backend/Makefile index 0ffc557..039340c 100644 --- a/backend/Makefile +++ b/backend/Makefile @@ -34,7 +34,6 @@ clean: rm -f plantuml.jar rm -f erd.png rm -f config.toml - rm -f database.txt # Test target test: db.sqlite3 @@ -106,7 +105,6 @@ docs: swag init -outputTypes go api: ./docs/swagger.json - rm ../frontend/src/API/GenApi.ts npx swagger-typescript-api \ --api-class-name GenApi \ --path ./docs/swagger.json \ diff --git a/backend/docs/docs.go b/backend/docs/docs.go index 7a08b0e..0009c17 100644 --- a/backend/docs/docs.go +++ b/backend/docs/docs.go @@ -21,21 +21,21 @@ const docTemplate = `{ "paths": { "/login": { "post": { - "description": "Logs in a user and returns a JWT token", + "description": "logs the user in and returns a jwt token", "consumes": [ "application/json" ], "produces": [ - "application/json" + "text/plain" ], "tags": [ - "Auth" + "User" ], - "summary": "Login", + "summary": "login", "parameters": [ { - "description": "User credentials", - "name": "body", + "description": "login info", + "name": "NewUser", "in": "body", "required": true, "schema": { @@ -45,9 +45,9 @@ const docTemplate = `{ ], "responses": { "200": { - "description": "JWT token", + "description": "Successfully signed token for user", "schema": { - "$ref": "#/definitions/types.Token" + "type": "Token" } }, "400": { @@ -71,26 +71,29 @@ const docTemplate = `{ } } }, - "/loginrenew": { + "/loginerenew": { "post": { "security": [ { - "JWT": [] + "bererToken": [] } ], - "description": "Renews the users token.", - "produces": [ + "description": "renews the users token", + "consumes": [ "application/json" ], + "produces": [ + "text/plain" + ], "tags": [ - "Auth" + "User" ], "summary": "LoginRenews", "responses": { "200": { "description": "Successfully signed token for user", "schema": { - "$ref": "#/definitions/types.Token" + "type": "Token" } }, "401": { @@ -110,12 +113,7 @@ const docTemplate = `{ }, "/promoteToAdmin": { "post": { - "security": [ - { - "JWT": [] - } - ], - "description": "Promote chosen user to site admin", + "description": "promote chosen user to admin", "consumes": [ "application/json" ], @@ -141,7 +139,7 @@ const docTemplate = `{ "200": { "description": "Successfully promoted user", "schema": { - "$ref": "#/definitions/types.Token" + "type": "json" } }, "400": { @@ -175,7 +173,7 @@ const docTemplate = `{ "text/plain" ], "tags": [ - "Auth" + "User" ], "summary": "Register", "parameters": [ @@ -213,11 +211,6 @@ const docTemplate = `{ }, "/userdelete/{username}": { "delete": { - "security": [ - { - "JWT": [] - } - ], "description": "UserDelete deletes a user from the database", "consumes": [ "application/json" @@ -259,27 +252,22 @@ const docTemplate = `{ }, "/users/all": { "get": { - "security": [ - { - "JWT": [] - } - ], "description": "lists all users", - "produces": [ + "consumes": [ "application/json" ], + "produces": [ + "text/plain" + ], "tags": [ "User" ], "summary": "ListsAllUsers", "responses": { "200": { - "description": "Successfully returned all users", + "description": "Successfully signed token for user", "schema": { - "type": "array", - "items": { - "type": "string" - } + "type": "json" } }, "401": { @@ -303,27 +291,16 @@ const docTemplate = `{ "type": "object", "properties": { "password": { - "type": "string", - "example": "password123" + "type": "string" }, "username": { - "type": "string", - "example": "username123" - } - } - }, - "types.Token": { - "type": "object", - "properties": { - "token": { "type": "string" } } } }, "securityDefinitions": { - "JWT": { - "description": "Use the JWT token provided by the login endpoint to authenticate requests. **Prefix the token with \"Bearer \".**", + "bererToken": { "type": "apiKey", "name": "Authorization", "in": "header" diff --git a/backend/internal/handlers/users/ListAllUsers.go b/backend/internal/handlers/users/ListAllUsers.go index 5ac5df0..1cae76c 100644 --- a/backend/internal/handlers/users/ListAllUsers.go +++ b/backend/internal/handlers/users/ListAllUsers.go @@ -7,17 +7,16 @@ import ( "github.com/gofiber/fiber/v2/log" ) -// @Summary ListsAllUsers -// @Description lists all users -// @Tags User -// @Produce json -// @Security JWT -// @Success 200 {array} string "Successfully returned all users" -// @Failure 401 {string} string "Unauthorized" -// @Failure 500 {string} string "Internal server error" -// @Router /users/all [get] -// -// ListAllUsers returns a list of all users in the application database +// ListAllUsers is a handler that returns a list of all users in the application database +// @Summary ListsAllUsers +// @Description lists all users +// @Tags User +// @Accept json +// @Produce plain +// @Success 200 {json} json "Successfully signed token for user" +// @Failure 401 {string} string "Unauthorized" +// @Failure 500 {string} string "Internal server error" +// @Router /users/all [get] func ListAllUsers(c *fiber.Ctx) error { // Get all users from the database users, err := db.GetDb(c).GetAllUsersApplication() diff --git a/backend/internal/handlers/users/Login.go b/backend/internal/handlers/users/Login.go index 42c52a5..c4d6c60 100644 --- a/backend/internal/handlers/users/Login.go +++ b/backend/internal/handlers/users/Login.go @@ -10,19 +10,18 @@ import ( "github.com/golang-jwt/jwt/v5" ) -// @Summary Login -// @Description Logs in a user and returns a JWT token -// @Tags Auth -// @Accept json -// @Produce json -// @Param body body types.NewUser true "User credentials" -// @Success 200 {object} types.Token "JWT token" -// @Failure 400 {string} string "Bad request" -// @Failure 401 {string} string "Unauthorized" -// @Failure 500 {string} string "Internal server error" -// @Router /login [post] -// -// Login logs in a user and returns a JWT token +// Login is a simple login handler that returns a JWT token +// @Summary login +// @Description logs the user in and returns a jwt token +// @Tags User +// @Accept json +// @Param NewUser body types.NewUser true "login info" +// @Produce plain +// @Success 200 Token types.Token "Successfully signed token for user" +// @Failure 400 {string} string "Bad request" +// @Failure 401 {string} string "Unauthorized" +// @Failure 500 {string} string "Internal server error" +// @Router /login [post] func Login(c *fiber.Ctx) error { // The body type is identical to a NewUser diff --git a/backend/internal/handlers/users/LoginRenew.go b/backend/internal/handlers/users/LoginRenew.go index 3926ce4..78eadfd 100644 --- a/backend/internal/handlers/users/LoginRenew.go +++ b/backend/internal/handlers/users/LoginRenew.go @@ -9,40 +9,34 @@ import ( "github.com/golang-jwt/jwt/v5" ) -// @Summary LoginRenews -// @Description Renews the users token. -// @Tags Auth -// @Produce json -// @Security JWT -// @Success 200 {object} types.Token "Successfully signed token for user" -// @Failure 401 {string} string "Unauthorized" -// @Failure 500 {string} string "Internal server error" -// @Router /loginrenew [post] -// -// LoginRenew renews the users token +// LoginRenew is a simple handler that renews the token +// @Summary LoginRenews +// @Description renews the users token +// @Security bererToken +// @Tags User +// @Accept json +// @Produce plain +// @Success 200 Token types.Token "Successfully signed token for user" +// @Failure 401 {string} string "Unauthorized" +// @Failure 500 {string} string "Internal server error" +// @Router /loginerenew [post] func LoginRenew(c *fiber.Ctx) error { user := c.Locals("user").(*jwt.Token) log.Info("Renewing token for user:", user.Claims.(jwt.MapClaims)["name"]) - // Renewing the token means we trust whatever is already in the token claims := user.Claims.(jwt.MapClaims) - - // 72 hour expiration time claims["exp"] = time.Now().Add(time.Hour * 72).Unix() - - // Create token with old claims, but new expiration time - token := jwt.NewWithClaims(jwt.SigningMethodHS256, jwt.MapClaims{ + renewed := jwt.MapClaims{ "name": claims["name"], "admin": claims["admin"], "exp": claims["exp"], - }) - - // Sign it with top secret key + } + token := jwt.NewWithClaims(jwt.SigningMethodHS256, renewed) t, err := token.SignedString([]byte("secret")) if err != nil { log.Warn("Error signing token") - return c.SendStatus(fiber.StatusInternalServerError) // 500 + return c.SendStatus(fiber.StatusInternalServerError) } log.Info("Successfully renewed token for user:", user.Claims.(jwt.MapClaims)["name"]) diff --git a/backend/internal/handlers/users/PromoteToAdmin.go b/backend/internal/handlers/users/PromoteToAdmin.go index 3f0a6d3..4a21758 100644 --- a/backend/internal/handlers/users/PromoteToAdmin.go +++ b/backend/internal/handlers/users/PromoteToAdmin.go @@ -8,20 +8,17 @@ import ( "github.com/gofiber/fiber/v2/log" ) -// @Summary PromoteToAdmin -// @Description Promote chosen user to site admin -// @Tags User -// @Accept json -// @Produce plain -// @Security JWT -// @Param NewUser body types.NewUser true "user info" -// @Success 200 {object} types.Token "Successfully promoted user" -// @Failure 400 {string} string "Bad request" -// @Failure 401 {string} string "Unauthorized" -// @Failure 500 {string} string "Internal server error" -// @Router /promoteToAdmin [post] -// -// PromoteToAdmin promotes a user to a site admin +// @Summary PromoteToAdmin +// @Description promote chosen user to admin +// @Tags User +// @Accept json +// @Produce plain +// @Param NewUser body types.NewUser true "user info" +// @Success 200 {json} json "Successfully promoted user" +// @Failure 400 {string} string "Bad request" +// @Failure 401 {string} string "Unauthorized" +// @Failure 500 {string} string "Internal server error" +// @Router /promoteToAdmin [post] func PromoteToAdmin(c *fiber.Ctx) error { // Extract the username from the request body var newUser types.NewUser diff --git a/backend/internal/handlers/users/Register.go b/backend/internal/handlers/users/Register.go index b9e0c78..9977246 100644 --- a/backend/internal/handlers/users/Register.go +++ b/backend/internal/handlers/users/Register.go @@ -8,9 +8,11 @@ import ( "github.com/gofiber/fiber/v2/log" ) +// Register is a simple handler that registers a new user +// // @Summary Register // @Description Register a new user -// @Tags Auth +// @Tags User // @Accept json // @Produce plain // @Param NewUser body types.NewUser true "User to register" @@ -18,8 +20,6 @@ import ( // @Failure 400 {string} string "Bad request" // @Failure 500 {string} string "Internal server error" // @Router /register [post] -// -// Register is a simple handler that registers a new user func Register(c *fiber.Ctx) error { u := new(types.NewUser) if err := c.BodyParser(u); err != nil { diff --git a/backend/internal/handlers/users/UserDelete.go b/backend/internal/handlers/users/UserDelete.go index 491a1b3..5957c2d 100644 --- a/backend/internal/handlers/users/UserDelete.go +++ b/backend/internal/handlers/users/UserDelete.go @@ -8,19 +8,19 @@ import ( "github.com/golang-jwt/jwt/v5" ) +// This path should obviously be protected in the future +// UserDelete deletes a user from the database +// // @Summary UserDelete // @Description UserDelete deletes a user from the database // @Tags User // @Accept json // @Produce plain -// @Security JWT // @Success 200 {string} string "User deleted" // @Failure 403 {string} string "You can only delete yourself" // @Failure 500 {string} string "Internal server error" // @Failure 401 {string} string "Unauthorized" // @Router /userdelete/{username} [delete] -// -// UserDelete deletes a user from the database func UserDelete(c *fiber.Ctx) error { // Read from path parameters username := c.Params("username") diff --git a/backend/internal/types/users.go b/backend/internal/types/users.go index 37cc8c2..88b4f06 100644 --- a/backend/internal/types/users.go +++ b/backend/internal/types/users.go @@ -18,8 +18,8 @@ func (u *User) ToPublicUser() (*PublicUser, error) { // Should be used when registering, for example type NewUser struct { - Username string `json:"username" example:"username123"` - Password string `json:"password" example:"password123"` + Username string `json:"username"` + Password string `json:"password"` } // PublicUser represents a user that is safe to send over the API (no password) diff --git a/backend/main.go b/backend/main.go index cf58280..4c2056e 100644 --- a/backend/main.go +++ b/backend/main.go @@ -25,16 +25,15 @@ import ( // @license.name AGPL // @license.url https://www.gnu.org/licenses/agpl-3.0.html -// @securityDefinitions.apikey JWT -// @in header -// @name Authorization -// @description Use the JWT token provided by the login endpoint to authenticate requests. **Prefix the token with "Bearer ".** +//@securityDefinitions.apikey bererToken +//@in header +//@name Authorization // @host localhost:8080 // @BasePath /api -// @externalDocs.description OpenAPI -// @externalDocs.url https://swagger.io/resources/open-api/ +// @externalDocs.description OpenAPI +// @externalDocs.url https://swagger.io/resources/open-api/ /** Main function for starting the server and initializing configurations. diff --git a/frontend/.prettierignore b/frontend/.prettierignore deleted file mode 100644 index c49d006..0000000 --- a/frontend/.prettierignore +++ /dev/null @@ -1,2 +0,0 @@ -goTypes.ts -GenApi.ts \ No newline at end of file diff --git a/frontend/src/API/GenApi.ts b/frontend/src/API/GenApi.ts deleted file mode 100644 index 8ca851b..0000000 --- a/frontend/src/API/GenApi.ts +++ /dev/null @@ -1,358 +0,0 @@ -/* eslint-disable */ -/* tslint:disable */ -/* - * --------------------------------------------------------------- - * ## THIS FILE WAS GENERATED VIA SWAGGER-TYPESCRIPT-API ## - * ## ## - * ## AUTHOR: acacode ## - * ## SOURCE: https://github.com/acacode/swagger-typescript-api ## - * --------------------------------------------------------------- - */ - -export interface TypesNewUser { - /** @example "password123" */ - password?: string; - /** @example "username123" */ - username?: string; -} - -export interface TypesToken { - token?: string; -} - -export type QueryParamsType = Record; -export type ResponseFormat = keyof Omit; - -export interface FullRequestParams extends Omit { - /** set parameter to `true` for call `securityWorker` for this request */ - secure?: boolean; - /** request path */ - path: string; - /** content type of request body */ - type?: ContentType; - /** query params */ - query?: QueryParamsType; - /** format of response (i.e. response.json() -> format: "json") */ - format?: ResponseFormat; - /** request body */ - body?: unknown; - /** base url */ - baseUrl?: string; - /** request cancellation token */ - cancelToken?: CancelToken; -} - -export type RequestParams = Omit; - -export interface ApiConfig { - baseUrl?: string; - baseApiParams?: Omit; - securityWorker?: (securityData: SecurityDataType | null) => Promise | RequestParams | void; - customFetch?: typeof fetch; -} - -export interface HttpResponse extends Response { - data: D; - error: E; -} - -type CancelToken = Symbol | string | number; - -export enum ContentType { - Json = "application/json", - FormData = "multipart/form-data", - UrlEncoded = "application/x-www-form-urlencoded", - Text = "text/plain", -} - -export class HttpClient { - public baseUrl: string = "//localhost:8080/api"; - private securityData: SecurityDataType | null = null; - private securityWorker?: ApiConfig["securityWorker"]; - private abortControllers = new Map(); - private customFetch = (...fetchParams: Parameters) => fetch(...fetchParams); - - private baseApiParams: RequestParams = { - credentials: "same-origin", - headers: {}, - redirect: "follow", - referrerPolicy: "no-referrer", - }; - - constructor(apiConfig: ApiConfig = {}) { - Object.assign(this, apiConfig); - } - - public setSecurityData = (data: SecurityDataType | null) => { - this.securityData = data; - }; - - protected encodeQueryParam(key: string, value: any) { - const encodedKey = encodeURIComponent(key); - return `${encodedKey}=${encodeURIComponent(typeof value === "number" ? value : `${value}`)}`; - } - - protected addQueryParam(query: QueryParamsType, key: string) { - return this.encodeQueryParam(key, query[key]); - } - - protected addArrayQueryParam(query: QueryParamsType, key: string) { - const value = query[key]; - return value.map((v: any) => this.encodeQueryParam(key, v)).join("&"); - } - - protected toQueryString(rawQuery?: QueryParamsType): string { - const query = rawQuery || {}; - const keys = Object.keys(query).filter((key) => "undefined" !== typeof query[key]); - return keys - .map((key) => (Array.isArray(query[key]) ? this.addArrayQueryParam(query, key) : this.addQueryParam(query, key))) - .join("&"); - } - - protected addQueryParams(rawQuery?: QueryParamsType): string { - const queryString = this.toQueryString(rawQuery); - return queryString ? `?${queryString}` : ""; - } - - private contentFormatters: Record any> = { - [ContentType.Json]: (input: any) => - input !== null && (typeof input === "object" || typeof input === "string") ? JSON.stringify(input) : input, - [ContentType.Text]: (input: any) => (input !== null && typeof input !== "string" ? JSON.stringify(input) : input), - [ContentType.FormData]: (input: any) => - Object.keys(input || {}).reduce((formData, key) => { - const property = input[key]; - formData.append( - key, - property instanceof Blob - ? property - : typeof property === "object" && property !== null - ? JSON.stringify(property) - : `${property}`, - ); - return formData; - }, new FormData()), - [ContentType.UrlEncoded]: (input: any) => this.toQueryString(input), - }; - - protected mergeRequestParams(params1: RequestParams, params2?: RequestParams): RequestParams { - return { - ...this.baseApiParams, - ...params1, - ...(params2 || {}), - headers: { - ...(this.baseApiParams.headers || {}), - ...(params1.headers || {}), - ...((params2 && params2.headers) || {}), - }, - }; - } - - protected createAbortSignal = (cancelToken: CancelToken): AbortSignal | undefined => { - if (this.abortControllers.has(cancelToken)) { - const abortController = this.abortControllers.get(cancelToken); - if (abortController) { - return abortController.signal; - } - return void 0; - } - - const abortController = new AbortController(); - this.abortControllers.set(cancelToken, abortController); - return abortController.signal; - }; - - public abortRequest = (cancelToken: CancelToken) => { - const abortController = this.abortControllers.get(cancelToken); - - if (abortController) { - abortController.abort(); - this.abortControllers.delete(cancelToken); - } - }; - - public request = async ({ - body, - secure, - path, - type, - query, - format, - baseUrl, - cancelToken, - ...params - }: FullRequestParams): Promise> => { - const secureParams = - ((typeof secure === "boolean" ? secure : this.baseApiParams.secure) && - this.securityWorker && - (await this.securityWorker(this.securityData))) || - {}; - const requestParams = this.mergeRequestParams(params, secureParams); - const queryString = query && this.toQueryString(query); - const payloadFormatter = this.contentFormatters[type || ContentType.Json]; - const responseFormat = format || requestParams.format; - - return this.customFetch(`${baseUrl || this.baseUrl || ""}${path}${queryString ? `?${queryString}` : ""}`, { - ...requestParams, - headers: { - ...(requestParams.headers || {}), - ...(type && type !== ContentType.FormData ? { "Content-Type": type } : {}), - }, - signal: (cancelToken ? this.createAbortSignal(cancelToken) : requestParams.signal) || null, - body: typeof body === "undefined" || body === null ? null : payloadFormatter(body), - }).then(async (response) => { - const r = response as HttpResponse; - r.data = null as unknown as T; - r.error = null as unknown as E; - - const data = !responseFormat - ? r - : await response[responseFormat]() - .then((data) => { - if (r.ok) { - r.data = data; - } else { - r.error = data; - } - return r; - }) - .catch((e) => { - r.error = e; - return r; - }); - - if (cancelToken) { - this.abortControllers.delete(cancelToken); - } - - if (!response.ok) throw data; - return data; - }); - }; -} - -/** - * @title TTime API - * @version 0.0.1 - * @license AGPL (https://www.gnu.org/licenses/agpl-3.0.html) - * @baseUrl //localhost:8080/api - * @externalDocs https://swagger.io/resources/open-api/ - * @contact - * - * This is the API for TTime, a time tracking application. - */ -export class GenApi extends HttpClient { - login = { - /** - * @description Logs in a user and returns a JWT token - * - * @tags Auth - * @name LoginCreate - * @summary Login - * @request POST:/login - */ - loginCreate: (body: TypesNewUser, params: RequestParams = {}) => - this.request({ - path: `/login`, - method: "POST", - body: body, - type: ContentType.Json, - format: "json", - ...params, - }), - }; - loginrenew = { - /** - * @description Renews the users token. - * - * @tags Auth - * @name LoginrenewCreate - * @summary LoginRenews - * @request POST:/loginrenew - * @secure - */ - loginrenewCreate: (params: RequestParams = {}) => - this.request({ - path: `/loginrenew`, - method: "POST", - secure: true, - format: "json", - ...params, - }), - }; - promoteToAdmin = { - /** - * @description Promote chosen user to site admin - * - * @tags User - * @name PromoteToAdminCreate - * @summary PromoteToAdmin - * @request POST:/promoteToAdmin - * @secure - */ - promoteToAdminCreate: (NewUser: TypesNewUser, params: RequestParams = {}) => - this.request({ - path: `/promoteToAdmin`, - method: "POST", - body: NewUser, - secure: true, - type: ContentType.Json, - ...params, - }), - }; - register = { - /** - * @description Register a new user - * - * @tags Auth - * @name RegisterCreate - * @summary Register - * @request POST:/register - */ - registerCreate: (NewUser: TypesNewUser, params: RequestParams = {}) => - this.request({ - path: `/register`, - method: "POST", - body: NewUser, - type: ContentType.Json, - ...params, - }), - }; - userdelete = { - /** - * @description UserDelete deletes a user from the database - * - * @tags User - * @name UserdeleteDelete - * @summary UserDelete - * @request DELETE:/userdelete/{username} - * @secure - */ - userdeleteDelete: (username: string, params: RequestParams = {}) => - this.request({ - path: `/userdelete/${username}`, - method: "DELETE", - secure: true, - type: ContentType.Json, - ...params, - }), - }; - users = { - /** - * @description lists all users - * - * @tags User - * @name GetUsers - * @summary ListsAllUsers - * @request GET:/users/all - * @secure - */ - getUsers: (params: RequestParams = {}) => - this.request({ - path: `/users/all`, - method: "GET", - secure: true, - format: "json", - ...params, - }), - }; -} diff --git a/frontend/src/Containers/GenApiDemo.tsx b/frontend/src/Containers/GenApiDemo.tsx deleted file mode 100644 index 27092d8..0000000 --- a/frontend/src/Containers/GenApiDemo.tsx +++ /dev/null @@ -1,23 +0,0 @@ -import { useEffect } from "react"; -import { GenApi } from "../API/GenApi"; - -// Instantiation of the API -const api = new GenApi(); - -export function GenApiDemo(): JSX.Element { - // Sync wrapper around the loginCreate method - const register = async (): Promise => { - const response = await api.login.loginCreate({ - username: "admin", - password: "admin", - }); - console.log(response.data); // This should be the inner type of the response - }; - - // Call the wrapper - useEffect(() => { - void register(); - }); - - return <>; -} diff --git a/frontend/src/Types/goTypes.ts b/frontend/src/Types/goTypes.ts index 7a15741..c519aac 100644 --- a/frontend/src/Types/goTypes.ts +++ b/frontend/src/Types/goTypes.ts @@ -124,44 +124,6 @@ export interface WeeklyReport { */ signedBy?: number /* int */; } -export interface UpdateWeeklyReport { - /** - * The name of the project, as it appears in the database - */ - projectName: string; - /** - * The name of the user - */ - userName: string; - /** - * The week number - */ - week: number /* int */; - /** - * Total time spent on development - */ - developmentTime: number /* int */; - /** - * Total time spent in meetings - */ - meetingTime: number /* int */; - /** - * Total time spent on administrative tasks - */ - adminTime: number /* int */; - /** - * Total time spent on personal projects - */ - ownWorkTime: number /* int */; - /** - * Total time spent on studying - */ - studyTime: number /* int */; - /** - * Total time spent on testing - */ - testingTime: number /* int */; -} ////////// // source: project.go @@ -189,9 +151,16 @@ export interface NewProject { */ export interface RoleChange { username: string; - role: 'project_manager' | 'user'; + role: "project_manager" | "user"; projectname: string; } + +export interface NewProjMember { + username: string; + projectname: string; + role: string; +} + export interface NameChange { id: number /* int */; name: string; @@ -222,6 +191,11 @@ export interface PublicUser { userId: string; username: string; } + +export interface UserProjectMember { + Username: string; + UserRole: string; +} /** * wrapper type for token */