mirror of
https://github.com/plankanban/planka.git
synced 2025-07-18 20:59:44 +02:00
feat: Invalidate access token on logout
This commit is contained in:
parent
f091de6827
commit
48ea62c0a0
26 changed files with 242 additions and 37 deletions
|
@ -44,6 +44,11 @@ const logout = () => ({
|
|||
payload: {},
|
||||
});
|
||||
|
||||
logout.invalidateAccessToken = () => ({
|
||||
type: ActionTypes.LOGOUT__ACCESS_TOKEN_INVALIDATE,
|
||||
payload: {},
|
||||
});
|
||||
|
||||
export default {
|
||||
initializeCore,
|
||||
logout,
|
||||
|
|
|
@ -103,10 +103,11 @@ const updateUserPassword = (id, data) => ({
|
|||
},
|
||||
});
|
||||
|
||||
updateUserPassword.success = (user) => ({
|
||||
updateUserPassword.success = (user, accessToken) => ({
|
||||
type: ActionTypes.USER_PASSWORD_UPDATE__SUCCESS,
|
||||
payload: {
|
||||
user,
|
||||
accessToken,
|
||||
},
|
||||
});
|
||||
|
||||
|
|
|
@ -1,9 +1,14 @@
|
|||
import http from './http';
|
||||
import socket from './socket';
|
||||
|
||||
/* Actions */
|
||||
|
||||
const createAccessToken = (data, headers) => http.post('/access-tokens', data, headers);
|
||||
|
||||
const deleteCurrentAccessToken = (headers) =>
|
||||
socket.delete('/access-tokens/me', undefined, headers);
|
||||
|
||||
export default {
|
||||
createAccessToken,
|
||||
deleteCurrentAccessToken,
|
||||
};
|
||||
|
|
|
@ -15,6 +15,7 @@ const Header = React.memo(
|
|||
project,
|
||||
user,
|
||||
notifications,
|
||||
isLogouting,
|
||||
canEditProject,
|
||||
canEditUsers,
|
||||
onProjectSettingsClick,
|
||||
|
@ -75,7 +76,11 @@ const Header = React.memo(
|
|||
)}
|
||||
</Menu.Item>
|
||||
</NotificationsPopup>
|
||||
<UserPopup onSettingsClick={onUserSettingsClick} onLogout={onLogout}>
|
||||
<UserPopup
|
||||
isLogouting={isLogouting}
|
||||
onSettingsClick={onUserSettingsClick}
|
||||
onLogout={onLogout}
|
||||
>
|
||||
<Menu.Item className={classNames(styles.item, styles.itemHoverable)}>
|
||||
{user.name}
|
||||
</Menu.Item>
|
||||
|
@ -93,6 +98,7 @@ Header.propTypes = {
|
|||
user: PropTypes.object.isRequired,
|
||||
notifications: PropTypes.array.isRequired,
|
||||
/* eslint-enable react/forbid-prop-types */
|
||||
isLogouting: PropTypes.bool.isRequired,
|
||||
canEditProject: PropTypes.bool.isRequired,
|
||||
canEditUsers: PropTypes.bool.isRequired,
|
||||
onProjectSettingsClick: PropTypes.func.isRequired,
|
||||
|
|
|
@ -1,13 +1,13 @@
|
|||
import React, { useCallback } from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { Menu } from 'semantic-ui-react';
|
||||
import { Button, Menu } from 'semantic-ui-react';
|
||||
import { withPopup } from '../../lib/popup';
|
||||
import { Popup } from '../../lib/custom-ui';
|
||||
|
||||
import styles from './UserPopup.module.scss';
|
||||
|
||||
const UserStep = React.memo(({ onSettingsClick, onLogout, onClose }) => {
|
||||
const UserStep = React.memo(({ isLogouting, onSettingsClick, onLogout, onClose }) => {
|
||||
const [t] = useTranslation();
|
||||
|
||||
const handleSettingsClick = useCallback(() => {
|
||||
|
@ -15,6 +15,17 @@ const UserStep = React.memo(({ onSettingsClick, onLogout, onClose }) => {
|
|||
onClose();
|
||||
}, [onSettingsClick, onClose]);
|
||||
|
||||
let logoutMenuItemProps;
|
||||
if (isLogouting) {
|
||||
logoutMenuItemProps = {
|
||||
as: Button,
|
||||
fluid: true,
|
||||
basic: true,
|
||||
loading: true,
|
||||
disabled: true,
|
||||
};
|
||||
}
|
||||
|
||||
return (
|
||||
<>
|
||||
<Popup.Header>
|
||||
|
@ -29,7 +40,11 @@ const UserStep = React.memo(({ onSettingsClick, onLogout, onClose }) => {
|
|||
context: 'title',
|
||||
})}
|
||||
</Menu.Item>
|
||||
<Menu.Item className={styles.menuItem} onClick={onLogout}>
|
||||
<Menu.Item
|
||||
{...logoutMenuItemProps} // eslint-disable-line react/jsx-props-no-spreading
|
||||
className={styles.menuItem}
|
||||
onClick={onLogout}
|
||||
>
|
||||
{t('action.logOut', {
|
||||
context: 'title',
|
||||
})}
|
||||
|
@ -41,6 +56,7 @@ const UserStep = React.memo(({ onSettingsClick, onLogout, onClose }) => {
|
|||
});
|
||||
|
||||
UserStep.propTypes = {
|
||||
isLogouting: PropTypes.bool.isRequired,
|
||||
onSettingsClick: PropTypes.func.isRequired,
|
||||
onLogout: PropTypes.func.isRequired,
|
||||
onClose: PropTypes.func.isRequired,
|
||||
|
|
|
@ -21,6 +21,7 @@ export default {
|
|||
|
||||
CORE_INITIALIZE: 'CORE_INITIALIZE',
|
||||
LOGOUT: 'LOGOUT',
|
||||
LOGOUT__ACCESS_TOKEN_INVALIDATE: 'LOGOUT__ACCESS_TOKEN_INVALIDATE',
|
||||
|
||||
/* Modals */
|
||||
|
||||
|
|
|
@ -6,6 +6,7 @@ import entryActions from '../entry-actions';
|
|||
import Header from '../components/Header';
|
||||
|
||||
const mapStateToProps = (state) => {
|
||||
const isLogouting = selectors.selectIsLogouting(state);
|
||||
const currentUser = selectors.selectCurrentUser(state);
|
||||
const currentProject = selectors.selectCurrentProject(state);
|
||||
const notifications = selectors.selectNotificationsForCurrentUser(state);
|
||||
|
@ -13,6 +14,7 @@ const mapStateToProps = (state) => {
|
|||
|
||||
return {
|
||||
notifications,
|
||||
isLogouting,
|
||||
project: currentProject,
|
||||
user: currentUser,
|
||||
canEditProject: isCurrentUserManager,
|
||||
|
|
|
@ -1,18 +1,34 @@
|
|||
import { getAccessToken } from '../utils/access-token-storage';
|
||||
import ActionTypes from '../constants/ActionTypes';
|
||||
|
||||
const initialState = {
|
||||
accessToken: getAccessToken(),
|
||||
userId: null,
|
||||
};
|
||||
|
||||
// eslint-disable-next-line default-param-last
|
||||
export default (state = initialState, { type, payload }) => {
|
||||
switch (type) {
|
||||
case ActionTypes.AUTHENTICATE__SUCCESS:
|
||||
return {
|
||||
...state,
|
||||
accessToken: payload.accessToken,
|
||||
};
|
||||
case ActionTypes.SOCKET_RECONNECT_HANDLE:
|
||||
case ActionTypes.CORE_INITIALIZE:
|
||||
return {
|
||||
...state,
|
||||
userId: payload.user.id,
|
||||
};
|
||||
case ActionTypes.USER_PASSWORD_UPDATE__SUCCESS:
|
||||
if (payload.accessToken) {
|
||||
return {
|
||||
...state,
|
||||
accessToken: payload.accessToken,
|
||||
};
|
||||
}
|
||||
|
||||
return state;
|
||||
default:
|
||||
return state;
|
||||
}
|
||||
|
|
|
@ -5,6 +5,7 @@ import ModalTypes from '../constants/ModalTypes';
|
|||
|
||||
const initialState = {
|
||||
isInitializing: true,
|
||||
isLogouting: false,
|
||||
currentModal: null,
|
||||
};
|
||||
|
||||
|
@ -22,6 +23,11 @@ export default (state = initialState, { type, payload }) => {
|
|||
...state,
|
||||
isInitializing: false,
|
||||
};
|
||||
case ActionTypes.LOGOUT__ACCESS_TOKEN_INVALIDATE:
|
||||
return {
|
||||
...state,
|
||||
isLogouting: true,
|
||||
};
|
||||
case ActionTypes.MODAL_OPEN:
|
||||
return {
|
||||
...state,
|
||||
|
|
|
@ -1,9 +1,8 @@
|
|||
import { all, apply, call, fork, take } from 'redux-saga/effects';
|
||||
import { all, apply, fork, take } from 'redux-saga/effects';
|
||||
|
||||
import watchers from './watchers';
|
||||
import services from './services';
|
||||
import { socket } from '../../api';
|
||||
import { removeAccessToken } from '../../utils/access-token-storage';
|
||||
import ActionTypes from '../../constants/ActionTypes';
|
||||
import Paths from '../../constants/Paths';
|
||||
|
||||
|
@ -15,6 +14,5 @@ export default function* coreSaga() {
|
|||
|
||||
yield take(ActionTypes.LOGOUT);
|
||||
|
||||
yield call(removeAccessToken);
|
||||
window.location.href = Paths.LOGIN;
|
||||
}
|
||||
|
|
|
@ -1,7 +1,8 @@
|
|||
import { call, fork, join, put, take } from 'redux-saga/effects';
|
||||
import { call, fork, join, put, select, take } from 'redux-saga/effects';
|
||||
|
||||
import selectors from '../../selectors';
|
||||
import actions from '../../actions';
|
||||
import { getAccessToken } from '../../utils/access-token-storage';
|
||||
import { removeAccessToken } from '../../utils/access-token-storage';
|
||||
import ErrorCodes from '../../constants/ErrorCodes';
|
||||
|
||||
let lastRequestTask;
|
||||
|
@ -13,7 +14,7 @@ function* queueRequest(method, ...args) {
|
|||
} catch {} // eslint-disable-line no-empty
|
||||
}
|
||||
|
||||
const accessToken = yield call(getAccessToken);
|
||||
const accessToken = yield select(selectors.selectAccessToken);
|
||||
|
||||
try {
|
||||
return yield call(method, ...args, {
|
||||
|
@ -21,6 +22,7 @@ function* queueRequest(method, ...args) {
|
|||
});
|
||||
} catch (error) {
|
||||
if (error.code === ErrorCodes.UNAUTHORIZED) {
|
||||
yield call(removeAccessToken);
|
||||
yield put(actions.logout()); // TODO: next url
|
||||
yield take();
|
||||
}
|
||||
|
|
|
@ -1,8 +1,11 @@
|
|||
import { call, put, take } from 'redux-saga/effects';
|
||||
|
||||
import request from '../request';
|
||||
import requests from '../requests';
|
||||
import actions from '../../../actions';
|
||||
import api from '../../../api';
|
||||
import i18n from '../../../i18n';
|
||||
import { removeAccessToken } from '../../../utils/access-token-storage';
|
||||
|
||||
export function* initializeCore() {
|
||||
const {
|
||||
|
@ -60,7 +63,17 @@ export function* changeCoreLanguage(language) {
|
|||
}
|
||||
}
|
||||
|
||||
export function* logout() {
|
||||
export function* logout(invalidateAccessToken = true) {
|
||||
yield call(removeAccessToken);
|
||||
|
||||
if (invalidateAccessToken) {
|
||||
yield put(actions.logout.invalidateAccessToken());
|
||||
|
||||
try {
|
||||
yield call(request, api.deleteCurrentAccessToken);
|
||||
} catch (error) {} // eslint-disable-line no-empty
|
||||
}
|
||||
|
||||
yield put(actions.logout());
|
||||
yield take();
|
||||
}
|
||||
|
|
|
@ -124,11 +124,13 @@ export function* updateUserPassword(id, data) {
|
|||
return;
|
||||
}
|
||||
|
||||
if (accessTokens && accessTokens[0]) {
|
||||
yield call(setAccessToken, accessTokens[0]);
|
||||
const accessToken = accessTokens && accessTokens[0];
|
||||
|
||||
if (accessToken) {
|
||||
yield call(setAccessToken, accessToken);
|
||||
}
|
||||
|
||||
yield put(actions.updateUserPassword.success(user));
|
||||
yield put(actions.updateUserPassword.success(user, accessToken));
|
||||
}
|
||||
|
||||
export function* updateCurrentUserPassword(data) {
|
||||
|
@ -215,7 +217,7 @@ export function* handleUserDelete(user) {
|
|||
const currentUserId = yield select(selectors.selectCurrentUserId);
|
||||
|
||||
if (user.id === currentUserId) {
|
||||
yield call(logout);
|
||||
yield call(logout, false);
|
||||
}
|
||||
|
||||
yield put(actions.handleUserDelete(user));
|
||||
|
|
|
@ -2,18 +2,13 @@ import { all, call, cancel, fork, take } from 'redux-saga/effects';
|
|||
|
||||
import watchers from './watchers';
|
||||
import services from './services';
|
||||
import { setAccessToken } from '../../utils/access-token-storage';
|
||||
import ActionTypes from '../../constants/ActionTypes';
|
||||
|
||||
export default function* loginSaga() {
|
||||
const watcherTasks = yield all(watchers.map((watcher) => fork(watcher)));
|
||||
|
||||
const {
|
||||
payload: { accessToken },
|
||||
} = yield take(ActionTypes.AUTHENTICATE__SUCCESS);
|
||||
yield take(ActionTypes.AUTHENTICATE__SUCCESS);
|
||||
|
||||
yield cancel(watcherTasks);
|
||||
|
||||
yield call(setAccessToken, accessToken);
|
||||
yield call(services.goToRoot);
|
||||
}
|
||||
|
|
|
@ -2,6 +2,7 @@ import { call, put } from 'redux-saga/effects';
|
|||
|
||||
import actions from '../../../actions';
|
||||
import api from '../../../api';
|
||||
import { setAccessToken } from '../../../utils/access-token-storage';
|
||||
|
||||
export function* authenticate(data) {
|
||||
yield put(actions.authenticate(data));
|
||||
|
@ -14,6 +15,7 @@ export function* authenticate(data) {
|
|||
return;
|
||||
}
|
||||
|
||||
yield call(setAccessToken, accessToken);
|
||||
yield put(actions.authenticate.success(accessToken));
|
||||
}
|
||||
|
||||
|
|
|
@ -1,5 +0,0 @@
|
|||
export const selectAccessToken = ({ auth: { accessToken } }) => accessToken;
|
||||
|
||||
export default {
|
||||
selectAccessToken,
|
||||
};
|
|
@ -4,8 +4,12 @@ import isUndefined from 'lodash/isUndefined';
|
|||
import orm from '../orm';
|
||||
import Config from '../constants/Config';
|
||||
|
||||
export const selectAccessToken = ({ auth: { accessToken } }) => accessToken;
|
||||
|
||||
export const selectIsCoreInitializing = ({ core: { isInitializing } }) => isInitializing;
|
||||
|
||||
export const selectIsLogouting = ({ core: { isLogouting } }) => isLogouting;
|
||||
|
||||
const nextPosition = (items, index, excludedId) => {
|
||||
const filteredItems = isUndefined(excludedId)
|
||||
? items
|
||||
|
@ -94,7 +98,9 @@ export const selectNextTaskPosition = createSelector(
|
|||
);
|
||||
|
||||
export default {
|
||||
selectAccessToken,
|
||||
selectIsCoreInitializing,
|
||||
selectIsLogouting,
|
||||
selectNextBoardPosition,
|
||||
selectNextListPosition,
|
||||
selectNextCardPosition,
|
||||
|
|
|
@ -1,5 +1,4 @@
|
|||
import router from './router';
|
||||
import auth from './auth';
|
||||
import core from './core';
|
||||
import modals from './modals';
|
||||
import users from './users';
|
||||
|
@ -14,7 +13,6 @@ import attachments from './attachments';
|
|||
|
||||
export default {
|
||||
...router,
|
||||
...auth,
|
||||
...core,
|
||||
...modals,
|
||||
...users,
|
||||
|
|
|
@ -40,24 +40,33 @@ module.exports = {
|
|||
},
|
||||
|
||||
async fn(inputs) {
|
||||
const remoteAddress = getRemoteAddress(this.req);
|
||||
|
||||
const user = await sails.helpers.users.getOneByEmailOrUsername(inputs.emailOrUsername);
|
||||
|
||||
if (!user) {
|
||||
sails.log.warn(
|
||||
`Invalid email or username: "${inputs.emailOrUsername}"! (IP: ${getRemoteAddress(
|
||||
this.req,
|
||||
)})`,
|
||||
`Invalid email or username: "${inputs.emailOrUsername}"! (IP: ${remoteAddress})`,
|
||||
);
|
||||
throw Errors.INVALID_EMAIL_OR_USERNAME;
|
||||
}
|
||||
|
||||
if (!bcrypt.compareSync(inputs.password, user.password)) {
|
||||
sails.log.warn(`Invalid password! (IP: ${getRemoteAddress(this.req)})`);
|
||||
sails.log.warn(`Invalid password! (IP: ${remoteAddress})`);
|
||||
throw Errors.INVALID_PASSWORD;
|
||||
}
|
||||
|
||||
const accessToken = sails.helpers.utils.createToken(user.id);
|
||||
|
||||
await Session.create({
|
||||
accessToken,
|
||||
remoteAddress,
|
||||
userId: user.id,
|
||||
userAgent: this.req.headers['user-agent'],
|
||||
});
|
||||
|
||||
return {
|
||||
item: sails.helpers.utils.createToken(user.id),
|
||||
item: accessToken,
|
||||
};
|
||||
},
|
||||
};
|
||||
|
|
16
server/api/controllers/access-tokens/delete.js
Normal file
16
server/api/controllers/access-tokens/delete.js
Normal file
|
@ -0,0 +1,16 @@
|
|||
module.exports = {
|
||||
async fn() {
|
||||
const { accessToken } = this.req;
|
||||
|
||||
await Session.updateOne({
|
||||
accessToken,
|
||||
deletedAt: null,
|
||||
}).set({
|
||||
deletedAt: new Date().toUTCString(),
|
||||
});
|
||||
|
||||
return {
|
||||
item: accessToken,
|
||||
};
|
||||
},
|
||||
};
|
|
@ -1,6 +1,8 @@
|
|||
const bcrypt = require('bcrypt');
|
||||
const zxcvbn = require('zxcvbn');
|
||||
|
||||
const { getRemoteAddress } = require('../../../utils/remoteAddress');
|
||||
|
||||
const Errors = {
|
||||
USER_NOT_FOUND: {
|
||||
userNotFound: 'User not found',
|
||||
|
@ -71,6 +73,13 @@ module.exports = {
|
|||
if (user.id === currentUser.id) {
|
||||
const accessToken = sails.helpers.utils.createToken(user.id, user.passwordUpdatedAt);
|
||||
|
||||
await Session.create({
|
||||
accessToken,
|
||||
userId: user.id,
|
||||
remoteAddress: getRemoteAddress(this.req),
|
||||
userAgent: this.req.headers['user-agent'],
|
||||
});
|
||||
|
||||
return {
|
||||
item: user,
|
||||
included: {
|
||||
|
|
|
@ -1,3 +1,4 @@
|
|||
const { v4: uuid } = require('uuid');
|
||||
const jwt = require('jsonwebtoken');
|
||||
|
||||
module.exports = {
|
||||
|
@ -24,6 +25,9 @@ module.exports = {
|
|||
exp: iat + sails.config.custom.tokenExpiresIn * 24 * 60 * 60,
|
||||
},
|
||||
sails.config.session.secret,
|
||||
{
|
||||
keyid: uuid(),
|
||||
},
|
||||
);
|
||||
},
|
||||
};
|
||||
|
|
|
@ -17,6 +17,15 @@ module.exports = function defineCurrentUserHook(sails) {
|
|||
return null;
|
||||
}
|
||||
|
||||
const session = await Session.findOne({
|
||||
accessToken,
|
||||
deletedAt: null,
|
||||
});
|
||||
|
||||
if (!session) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const user = await sails.helpers.users.getOne(payload.subject);
|
||||
|
||||
if (user && user.passwordChangedAt > payload.issuedAt) {
|
||||
|
@ -43,8 +52,14 @@ module.exports = function defineCurrentUserHook(sails) {
|
|||
|
||||
if (authorizationHeader && TOKEN_PATTERN.test(authorizationHeader)) {
|
||||
const accessToken = authorizationHeader.replace(TOKEN_PATTERN, '');
|
||||
const currentUser = await getUser(accessToken);
|
||||
|
||||
req.currentUser = await getUser(accessToken);
|
||||
if (currentUser) {
|
||||
Object.assign(req, {
|
||||
accessToken,
|
||||
currentUser,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
return next();
|
||||
|
@ -52,8 +67,17 @@ module.exports = function defineCurrentUserHook(sails) {
|
|||
},
|
||||
'/attachments/*': {
|
||||
async fn(req, res, next) {
|
||||
if (req.cookies.accessToken) {
|
||||
req.currentUser = await getUser(req.cookies.accessToken);
|
||||
const { accessToken } = req.cookies;
|
||||
|
||||
if (accessToken) {
|
||||
const currentUser = await getUser(accessToken);
|
||||
|
||||
if (currentUser) {
|
||||
Object.assign(req, {
|
||||
accessToken,
|
||||
currentUser,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
return next();
|
||||
|
|
49
server/api/models/Session.js
Executable file
49
server/api/models/Session.js
Executable file
|
@ -0,0 +1,49 @@
|
|||
/**
|
||||
* Session.js
|
||||
*
|
||||
* @description :: A model definition represents a database table/collection.
|
||||
* @docs :: https://sailsjs.com/docs/concepts/models-and-orm/models
|
||||
*/
|
||||
|
||||
module.exports = {
|
||||
attributes: {
|
||||
// ╔═╗╦═╗╦╔╦╗╦╔╦╗╦╦ ╦╔═╗╔═╗
|
||||
// ╠═╝╠╦╝║║║║║ ║ ║╚╗╔╝║╣ ╚═╗
|
||||
// ╩ ╩╚═╩╩ ╩╩ ╩ ╩ ╚╝ ╚═╝╚═╝
|
||||
|
||||
accessToken: {
|
||||
type: 'string',
|
||||
required: true,
|
||||
columnName: 'access_token',
|
||||
},
|
||||
remoteAddress: {
|
||||
type: 'string',
|
||||
required: true,
|
||||
columnName: 'remote_address',
|
||||
},
|
||||
userAgent: {
|
||||
type: 'string',
|
||||
isNotEmptyString: true,
|
||||
allowNull: true,
|
||||
columnName: 'user_agent',
|
||||
},
|
||||
deletedAt: {
|
||||
type: 'ref',
|
||||
columnName: 'deleted_at',
|
||||
},
|
||||
|
||||
// ╔═╗╔╦╗╔╗ ╔═╗╔╦╗╔═╗
|
||||
// ║╣ ║║║╠╩╗║╣ ║║╚═╗
|
||||
// ╚═╝╩ ╩╚═╝╚═╝═╩╝╚═╝
|
||||
|
||||
// ╔═╗╔═╗╔═╗╔═╗╔═╗╦╔═╗╔╦╗╦╔═╗╔╗╔╔═╗
|
||||
// ╠═╣╚═╗╚═╗║ ║║ ║╠═╣ ║ ║║ ║║║║╚═╗
|
||||
// ╩ ╩╚═╝╚═╝╚═╝╚═╝╩╩ ╩ ╩ ╩╚═╝╝╚╝╚═╝
|
||||
|
||||
userId: {
|
||||
model: 'User',
|
||||
required: true,
|
||||
columnName: 'user_id',
|
||||
},
|
||||
},
|
||||
};
|
|
@ -10,6 +10,7 @@
|
|||
|
||||
module.exports.routes = {
|
||||
'POST /api/access-tokens': 'access-tokens/create',
|
||||
'DELETE /api/access-tokens/me': 'access-tokens/delete',
|
||||
|
||||
'GET /api/users': 'users/index',
|
||||
'POST /api/users': 'users/create',
|
||||
|
|
24
server/db/migrations/20220906094517_create_session_table.js
Normal file
24
server/db/migrations/20220906094517_create_session_table.js
Normal file
|
@ -0,0 +1,24 @@
|
|||
module.exports.up = (knex) =>
|
||||
knex.schema.createTable('session', (table) => {
|
||||
/* Columns */
|
||||
|
||||
table.bigInteger('id').primary().defaultTo(knex.raw('next_id()'));
|
||||
|
||||
table.bigInteger('user_id').notNullable();
|
||||
|
||||
table.text('access_token').notNullable();
|
||||
table.text('remote_address').notNullable();
|
||||
table.text('user_agent');
|
||||
|
||||
table.timestamp('created_at', true);
|
||||
table.timestamp('updated_at', true);
|
||||
table.timestamp('deleted_at', true);
|
||||
|
||||
/* Indexes */
|
||||
|
||||
table.index('user_id');
|
||||
table.unique('access_token');
|
||||
table.index('remote_address');
|
||||
});
|
||||
|
||||
module.exports.down = (knex) => knex.schema.dropTable('session');
|
Loading…
Add table
Add a link
Reference in a new issue