11import express , { Request , Response } from 'express' ;
22import { utils } from 'ssh2' ;
3+ import crypto from 'crypto' ;
34
45import * as db from '../../db' ;
56import { toPublicUser } from './publicApi' ;
6- import { DuplicateSSHKeyError , UserNotFoundError } from '../../errors/DatabaseErrors' ;
77
88const router = express . Router ( ) ;
9- const parseKey = utils . parseKey ;
9+
10+ // Calculate SHA-256 fingerprint from SSH public key
11+ // Note: This function is duplicated in src/cli/ssh-key.ts to keep CLI and server independent
12+ function calculateFingerprint ( publicKeyStr : string ) : string | null {
13+ try {
14+ const parsed = utils . parseKey ( publicKeyStr ) ;
15+ if ( ! parsed || parsed instanceof Error ) {
16+ return null ;
17+ }
18+ const pubKey = parsed . getPublicSSH ( ) ;
19+ const hash = crypto . createHash ( 'sha256' ) . update ( pubKey ) . digest ( 'base64' ) ;
20+ return `SHA256:${ hash } ` ;
21+ } catch ( err ) {
22+ console . error ( 'Error calculating fingerprint:' , err ) ;
23+ return null ;
24+ }
25+ }
1026
1127router . get ( '/' , async ( req : Request , res : Response ) => {
1228 console . log ( 'fetching users' ) ;
@@ -25,91 +41,126 @@ router.get('/:id', async (req: Request, res: Response) => {
2541 res . send ( toPublicUser ( user ) ) ;
2642} ) ;
2743
44+ // Get SSH key fingerprints for a user
45+ router . get ( '/:username/ssh-key-fingerprints' , async ( req : Request , res : Response ) => {
46+ if ( ! req . user ) {
47+ res . status ( 401 ) . json ( { error : 'Authentication required' } ) ;
48+ return ;
49+ }
50+
51+ const { username, admin } = req . user as { username : string ; admin : boolean } ;
52+ const targetUsername = req . params . username . toLowerCase ( ) ;
53+
54+ // Only allow users to view their own keys, or admins to view any keys
55+ if ( username !== targetUsername && ! admin ) {
56+ res . status ( 403 ) . json ( { error : 'Not authorized to view keys for this user' } ) ;
57+ return ;
58+ }
59+
60+ try {
61+ const publicKeys = await db . getPublicKeys ( targetUsername ) ;
62+ const keyFingerprints = publicKeys . map ( ( keyRecord ) => ( {
63+ fingerprint : keyRecord . fingerprint ,
64+ name : keyRecord . name ,
65+ addedAt : keyRecord . addedAt ,
66+ } ) ) ;
67+ res . json ( keyFingerprints ) ;
68+ } catch ( error ) {
69+ console . error ( 'Error retrieving SSH keys:' , error ) ;
70+ res . status ( 500 ) . json ( { error : 'Failed to retrieve SSH keys' } ) ;
71+ }
72+ } ) ;
73+
2874// Add SSH public key
2975router . post ( '/:username/ssh-keys' , async ( req : Request , res : Response ) => {
3076 if ( ! req . user ) {
31- res . status ( 401 ) . json ( { error : 'Login required' } ) ;
77+ res . status ( 401 ) . json ( { error : 'Authentication required' } ) ;
3278 return ;
3379 }
3480
3581 const { username, admin } = req . user as { username : string ; admin : boolean } ;
3682 const targetUsername = req . params . username . toLowerCase ( ) ;
3783
38- // Admins can add to any account, users can only add to their own
84+ // Only allow users to add keys to their own account, or admins to add to any account
3985 if ( username !== targetUsername && ! admin ) {
4086 res . status ( 403 ) . json ( { error : 'Not authorized to add keys for this user' } ) ;
4187 return ;
4288 }
4389
44- const { publicKey } = req . body ;
45- if ( ! publicKey || typeof publicKey !== 'string' ) {
90+ const { publicKey, name } = req . body ;
91+ if ( ! publicKey ) {
4692 res . status ( 400 ) . json ( { error : 'Public key is required' } ) ;
4793 return ;
4894 }
4995
50- try {
51- const parsedKey = parseKey ( publicKey . trim ( ) ) ;
96+ // Strip the comment from the key (everything after the last space)
97+ const keyWithoutComment = publicKey . trim ( ) . split ( ' ' ) . slice ( 0 , 2 ) . join ( ' ' ) ;
5298
53- if ( parsedKey instanceof Error ) {
54- res . status ( 400 ) . json ( { error : `Invalid SSH key: ${ parsedKey . message } ` } ) ;
55- return ;
56- }
99+ // Calculate fingerprint
100+ const fingerprint = calculateFingerprint ( keyWithoutComment ) ;
101+ if ( ! fingerprint ) {
102+ res . status ( 400 ) . json ( { error : 'Invalid SSH public key format' } ) ;
103+ return ;
104+ }
57105
58- if ( parsedKey . isPrivateKey ( ) ) {
59- res . status ( 400 ) . json ( { error : 'Invalid SSH key: Must be a public key' } ) ;
60- return ;
61- }
106+ const publicKeyRecord = {
107+ key : keyWithoutComment ,
108+ name : name || 'Unnamed Key' ,
109+ addedAt : new Date ( ) . toISOString ( ) ,
110+ fingerprint : fingerprint ,
111+ } ;
62112
63- const keyWithoutComment = parsedKey . getPublicSSH ( ) . toString ( 'utf8' ) ;
64- console . log ( 'Adding SSH key' , { targetUsername, keyWithoutComment } ) ;
65- await db . addPublicKey ( targetUsername , keyWithoutComment ) ;
66- res . status ( 201 ) . json ( { message : 'SSH key added successfully' } ) ;
67- } catch ( error ) {
113+ console . log ( 'Adding SSH key' , { targetUsername, fingerprint } ) ;
114+ try {
115+ await db . addPublicKey ( targetUsername , publicKeyRecord ) ;
116+ res . status ( 201 ) . json ( {
117+ message : 'SSH key added successfully' ,
118+ fingerprint : fingerprint ,
119+ } ) ;
120+ } catch ( error : any ) {
68121 console . error ( 'Error adding SSH key:' , error ) ;
69122
70- if ( error instanceof DuplicateSSHKeyError ) {
71- res . status ( 409 ) . json ( { error : error . message } ) ;
72- return ;
73- }
74-
75- if ( error instanceof UserNotFoundError ) {
76- res . status ( 404 ) . json ( { error : error . message } ) ;
77- return ;
123+ // Return specific error message
124+ if ( error . message === 'SSH key already exists' ) {
125+ res . status ( 409 ) . json ( { error : 'This SSH key already exists' } ) ;
126+ } else if ( error . message === 'User not found' ) {
127+ res . status ( 404 ) . json ( { error : 'User not found' } ) ;
128+ } else {
129+ res . status ( 500 ) . json ( { error : error . message || 'Failed to add SSH key' } ) ;
78130 }
79-
80- const errorMessage = error instanceof Error ? error . message : 'Unknown error' ;
81- res . status ( 500 ) . json ( { error : `Failed to add SSH key: ${ errorMessage } ` } ) ;
82131 }
83132} ) ;
84133
85- // Remove SSH public key
86- router . delete ( '/:username/ssh-keys' , async ( req : Request , res : Response ) => {
134+ // Remove SSH public key by fingerprint
135+ router . delete ( '/:username/ssh-keys/:fingerprint ' , async ( req : Request , res : Response ) => {
87136 if ( ! req . user ) {
88- res . status ( 401 ) . json ( { error : 'Login required' } ) ;
137+ res . status ( 401 ) . json ( { error : 'Authentication required' } ) ;
89138 return ;
90139 }
91140
92141 const { username, admin } = req . user as { username : string ; admin : boolean } ;
93142 const targetUsername = req . params . username . toLowerCase ( ) ;
143+ const fingerprint = req . params . fingerprint ;
94144
95145 // Only allow users to remove keys from their own account, or admins to remove from any account
96146 if ( username !== targetUsername && ! admin ) {
97147 res . status ( 403 ) . json ( { error : 'Not authorized to remove keys for this user' } ) ;
98148 return ;
99149 }
100150
101- const { publicKey } = req . body ;
102- if ( ! publicKey ) {
103- res . status ( 400 ) . json ( { error : 'Public key is required' } ) ;
104- return ;
105- }
106-
151+ console . log ( 'Removing SSH key' , { targetUsername, fingerprint } ) ;
107152 try {
108- await db . removePublicKey ( targetUsername , publicKey ) ;
153+ await db . removePublicKey ( targetUsername , fingerprint ) ;
109154 res . status ( 200 ) . json ( { message : 'SSH key removed successfully' } ) ;
110- } catch ( error ) {
155+ } catch ( error : any ) {
111156 console . error ( 'Error removing SSH key:' , error ) ;
112- res . status ( 500 ) . json ( { error : 'Failed to remove SSH key' } ) ;
157+
158+ // Return specific error message
159+ if ( error . message === 'User not found' ) {
160+ res . status ( 404 ) . json ( { error : 'User not found' } ) ;
161+ } else {
162+ res . status ( 500 ) . json ( { error : error . message || 'Failed to remove SSH key' } ) ;
163+ }
113164 }
114165} ) ;
115166
0 commit comments